From 3ba51e4a31f21c1618edcb45a9daa2737568db10 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Wed, 12 Jul 2023 15:23:50 -0400 Subject: [PATCH] [Security Solution][Endpoint] Remove use of Redux from the Endpoint Policy Settings form (#161511) ## Summary - Re-creates all policy settings form components so that the Policy settings are provided as a prop - Adds `mode = view` to the policy form. When in this mode (user has no authz to edit), form will be displayed in view only mode (no more `disabled` form elements) --- .../policy/get_policy_data_for_update.ts | 7 +- .../e2e/mocked_data/policy_details.cy.ts | 88 ++-- .../cypress/screens/policy_details.ts | 11 +- .../hooks/policy/use_fetch_endpoint_policy.ts | 92 ++++ ...use_fetch_endpoint_policy_agent_summary.ts | 37 ++ .../policy/use_update_endpoint_policy.ts | 45 ++ .../policy/models/policy_details_config.ts | 23 - .../selectors/policy_settings_selectors.ts | 8 - .../view/{ => components}/agents_summary.tsx | 0 .../antivirus_registration_form/index.tsx | 82 --- .../attack_surface_reduction_form/index.tsx | 61 --- .../components/config_form/index.stories.tsx | 67 --- .../view/components/events_form/index.tsx | 197 -------- .../management/pages/policy/view/index.ts | 1 - .../endpoint_policy_edit_extension.test.tsx | 10 +- .../endpoint_policy_edit_extension.tsx | 166 ++----- .../pages/policy/view/policy_advanced.tsx | 213 -------- .../pages/policy/view/policy_details.test.tsx | 3 - .../pages/policy/view/policy_details.tsx | 2 +- .../pages/policy/view/policy_details_form.tsx | 131 ----- .../components/policy_form_layout.test.tsx | 470 ------------------ .../components/protection_radio.tsx | 98 ---- .../components/protection_switch.tsx | 134 ----- .../policy_forms/components/radio_buttons.tsx | 86 ---- .../components/supported_version.tsx | 30 -- .../components/user_notification.tsx | 210 -------- .../policy/view/policy_forms/locked_card.tsx | 93 ---- .../policy_forms/protections/behavior.tsx | 74 --- .../view/policy_forms/protections/malware.tsx | 162 ------ .../view/policy_forms/protections/memory.tsx | 74 --- .../protections/popup_options_to_versions.ts | 15 - .../policy_forms/protections/ransomware.tsx | 71 --- .../pages/policy/view/policy_hooks.ts | 30 +- .../components/advanced_section.tsx | 239 +++++++++ .../cards/antivirus_registration_card.tsx | 92 ++++ .../cards/attack_surface_reduction_card.tsx | 95 ++++ .../cards/behaviour_protection_card.tsx | 114 +++++ .../cards/event_collection_card.tsx | 283 +++++++++++ .../cards/linux_event_collection_card.tsx} | 79 +-- .../cards/mac_event_collection_card.tsx} | 30 +- .../cards/malware_protections_card.tsx | 205 ++++++++ .../cards/memory_protection_card.tsx | 116 +++++ .../cards/ransomware_protection_card.tsx | 114 +++++ .../cards/windows_event_collection_card.tsx} | 30 +- .../detect_prevent_protection_level.tsx | 185 +++++++ .../components/notify_user_option.tsx | 291 +++++++++++ .../protection_setting_card_switch.tsx | 140 ++++++ .../components/setting_card.tsx} | 34 +- .../components/setting_locked_card.tsx | 98 ++++ .../index.ts} | 2 +- .../policy/view/policy_settings_form/mocks.ts | 116 +++++ .../policy_settings_form.tsx | 87 ++++ ...ction_notice_supported_endpoint_version.ts | 13 + .../policy/view/policy_settings_form/types.ts | 15 + .../components/policy_form_confirm_update.tsx | 0 .../index.ts} | 4 +- .../policy_settings_layout.tsx} | 192 ++++--- .../pages/policy/view/tabs/policy_tabs.tsx | 11 +- .../management/services/policies/hooks.ts | 1 + .../management/services/policies/ingest.ts | 34 -- .../translations/translations/fr-FR.json | 7 - .../translations/translations/ja-JP.json | 7 - .../translations/translations/zh-CN.json | 7 - .../apps/integrations/policy_details.ts | 149 +++--- .../page_objects/policy_page.ts | 50 +- 65 files changed, 2795 insertions(+), 2836 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.ts create mode 100644 x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy_agent_summary.ts create mode 100644 x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts rename x-pack/plugins/security_solution/public/management/pages/policy/view/{ => components}/agents_summary.tsx (100%) delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/components/attack_surface_reduction_form/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/supported_version.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/event_collection_card.tsx rename x-pack/plugins/security_solution/public/management/pages/policy/view/{policy_forms/events/linux.tsx => policy_settings_form/components/cards/linux_event_collection_card.tsx} (65%) rename x-pack/plugins/security_solution/public/management/pages/policy/view/{policy_forms/events/mac.tsx => policy_settings_form/components/cards/mac_event_collection_card.tsx} (55%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.tsx rename x-pack/plugins/security_solution/public/management/pages/policy/view/{policy_forms/events/windows.tsx => policy_settings_form/components/cards/windows_event_collection_card.tsx} (71%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.tsx rename x-pack/plugins/security_solution/public/management/pages/policy/view/{components/config_form/index.tsx => policy_settings_form/components/setting_card.tsx} (77%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx rename x-pack/plugins/security_solution/public/management/pages/policy/view/{policy_forms/components/index.tsx => policy_settings_form/index.ts} (80%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/protection_notice_supported_endpoint_version.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/types.ts rename x-pack/plugins/security_solution/public/management/pages/policy/view/{policy_forms => policy_settings_layout}/components/policy_form_confirm_update.tsx (100%) rename x-pack/plugins/security_solution/public/management/pages/policy/view/{policy_forms/events/index.tsx => policy_settings_layout/index.ts} (68%) rename x-pack/plugins/security_solution/public/management/pages/policy/view/{policy_forms/components/policy_form_layout.tsx => policy_settings_layout/policy_settings_layout.tsx} (51%) diff --git a/x-pack/plugins/security_solution/common/endpoint/service/policy/get_policy_data_for_update.ts b/x-pack/plugins/security_solution/common/endpoint/service/policy/get_policy_data_for_update.ts index 3abf10c9c7147..ef45df503cdc3 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/policy/get_policy_data_for_update.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/policy/get_policy_data_for_update.ts @@ -15,9 +15,10 @@ import type { MaybeImmutable, NewPolicyData, PolicyData } from '../../types'; */ export const getPolicyDataForUpdate = (policy: MaybeImmutable): NewPolicyData => { // eslint-disable-next-line @typescript-eslint/naming-convention - const { id, revision, created_by, created_at, updated_by, updated_at, ...newPolicy } = policy; - // cast to `NewPolicyData` (mutable) since we cloned the entire object - const policyDataForUpdate = cloneDeep(newPolicy) as NewPolicyData; + const { id, revision, created_by, created_at, updated_by, updated_at, ...rest } = + policy as PolicyData; + + const policyDataForUpdate: NewPolicyData = cloneDeep(rest); const endpointPolicy = policyDataForUpdate.inputs[0].config.policy.value; // trim custom malware notification string diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/policy_details.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/policy_details.cy.ts index bcc68b642de84..543ce48ffb79d 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/policy_details.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/policy_details.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { getPolicySettingsFormTestSubjects } from '../../../pages/policy/view/policy_settings_form/mocks'; import { ProtectionModes } from '../../../../../common/endpoint/types'; import { PackagePolicyBackupHelper, @@ -30,7 +31,7 @@ describe('Policy Details', () => { beforeEach(() => { login(); - visitPolicyDetailsPage(); + visitPolicyDetailsPage(indexedHostsData.data.integrationPolicies[0].id); }); afterEach(() => { @@ -42,33 +43,43 @@ describe('Policy Details', () => { }); describe('Malware Protection card', () => { + const malwareTestSubj = getPolicySettingsFormTestSubjects().malware; + it('user should be able to see related rules', () => { - cy.getByTestSubj('malwareProtectionsForm').contains('related detection rules').click(); + cy.getByTestSubj(malwareTestSubj.card).contains('related detection rules').click(); cy.url().should('contain', 'app/security/rules/management'); }); it('changing protection level should enable or disable user notification', () => { - cy.getByTestSubj('malwareProtectionSwitch').click(); - cy.getByTestSubj('malwareProtectionSwitch').should('have.attr', 'aria-checked', 'true'); + cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).click(); + cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).should( + 'have.attr', + 'aria-checked', + 'true' + ); // Default: Prevent + Notify user enabled - cy.getByTestSubj('malwareProtectionMode_prevent').find('input').should('be.checked'); - cy.getByTestSubj('malwareUserNotificationCheckbox').should('be.checked'); + cy.getByTestSubj(malwareTestSubj.protectionPreventRadio).find('input').should('be.checked'); + cy.getByTestSubj(malwareTestSubj.notifyUserCheckbox).should('be.checked'); // Changing to Detect -> Notify user disabled - cy.getByTestSubj('malwareProtectionMode_detect').find('label').click(); - cy.getByTestSubj('malwareUserNotificationCheckbox').should('not.be.checked'); + cy.getByTestSubj(malwareTestSubj.protectionDetectRadio).find('label').click(); + cy.getByTestSubj(malwareTestSubj.notifyUserCheckbox).should('not.be.checked'); // Changing back to Prevent -> Notify user enabled - cy.getByTestSubj('malwareProtectionMode_prevent').find('label').click(); - cy.getByTestSubj('malwareUserNotificationCheckbox').should('be.checked'); + cy.getByTestSubj(malwareTestSubj.protectionPreventRadio).find('label').click(); + cy.getByTestSubj(malwareTestSubj.notifyUserCheckbox).should('be.checked'); }); it('disabling protection should disable notification in yaml for every OS', () => { // Enable malware protection and user notification - cy.getByTestSubj('malwareProtectionSwitch').click(); - cy.getByTestSubj('malwareProtectionSwitch').should('have.attr', 'aria-checked', 'true'); + cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).click(); + cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).should( + 'have.attr', + 'aria-checked', + 'true' + ); savePolicyForm(); yieldPolicyConfig().then((policyConfig) => { @@ -78,8 +89,12 @@ describe('Policy Details', () => { }); // disable malware protection - cy.getByTestSubj('malwareProtectionSwitch').click(); - cy.getByTestSubj('malwareProtectionSwitch').should('have.attr', 'aria-checked', 'false'); + cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).click(); + cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).should( + 'have.attr', + 'aria-checked', + 'false' + ); savePolicyForm(); yieldPolicyConfig().then((policyConfig) => { @@ -96,11 +111,12 @@ describe('Policy Details', () => { expect(policyConfig.windows.malware.mode).to.equal(ProtectionModes.off); }); - cy.getByTestSubj('malwareProtectionsForm').should('contain.text', 'Linux'); - cy.getByTestSubj('malwareProtectionsForm').should('contain.text', 'Windows'); - cy.getByTestSubj('malwareProtectionsForm').should('contain.text', 'Mac'); + cy.getByTestSubj(malwareTestSubj.osValuesContainer).should( + 'contain.text', + 'Windows, Mac, Linux' + ); - cy.getByTestSubj('malwareProtectionSwitch').click(); + cy.getByTestSubj(malwareTestSubj.enableDisableSwitch).click(); savePolicyForm(); yieldPolicyConfig().then((policyConfig) => { @@ -112,40 +128,50 @@ describe('Policy Details', () => { }); describe('Ransomware Protection card', () => { + const ransomwareTestSubj = getPolicySettingsFormTestSubjects().ransomware; + it('user should be able to see related rules', () => { - cy.getByTestSubj('ransomwareProtectionsForm').contains('related detection rules').click(); + cy.getByTestSubj(ransomwareTestSubj.card).contains('related detection rules').click(); cy.url().should('contain', 'app/security/rules/management'); }); it('changing protection level should enable or disable user notification', () => { - cy.getByTestSubj('ransomwareProtectionSwitch').click(); - cy.getByTestSubj('ransomwareProtectionSwitch').should('have.attr', 'aria-checked', 'true'); + cy.getByTestSubj(ransomwareTestSubj.enableDisableSwitch).click(); + cy.getByTestSubj(ransomwareTestSubj.enableDisableSwitch).should( + 'have.attr', + 'aria-checked', + 'true' + ); // Default: Prevent + Notify user enabled - cy.getByTestSubj('ransomwareProtectionMode_prevent').find('input').should('be.checked'); - cy.getByTestSubj('ransomwareUserNotificationCheckbox').should('be.checked'); + cy.getByTestSubj(ransomwareTestSubj.protectionPreventRadio) + .find('input') + .should('be.checked'); + cy.getByTestSubj(ransomwareTestSubj.notifyUserCheckbox).should('be.checked'); // Changing to Detect -> Notify user disabled - cy.getByTestSubj('ransomwareProtectionMode_detect').find('label').click(); - cy.getByTestSubj('ransomwareUserNotificationCheckbox').should('not.be.checked'); + cy.getByTestSubj(ransomwareTestSubj.protectionDetectRadio).find('label').click(); + cy.getByTestSubj(ransomwareTestSubj.notifyUserCheckbox).should('not.be.checked'); // Changing back to Prevent -> Notify user enabled - cy.getByTestSubj('ransomwareProtectionMode_prevent').find('label').click(); - cy.getByTestSubj('ransomwareUserNotificationCheckbox').should('be.checked'); + cy.getByTestSubj(ransomwareTestSubj.protectionPreventRadio).find('label').click(); + cy.getByTestSubj(ransomwareTestSubj.notifyUserCheckbox).should('be.checked'); }); }); describe('Advanced settings', () => { + const testSubjects = getPolicySettingsFormTestSubjects().advancedSection; + it('should show empty text inputs except for some settings', () => { const settingsWithDefaultValues = [ 'mac.advanced.capture_env_vars', 'linux.advanced.capture_env_vars', ]; - cy.getByTestSubj('advancedPolicyButton').click(); + cy.getByTestSubj(testSubjects.showHideButton).click(); - cy.getByTestSubj('advancedPolicyPanel') + cy.getByTestSubj(testSubjects.settingsContainer) .children() .each(($child) => { const settingName = $child.find('label').text(); @@ -167,8 +193,8 @@ describe('Policy Details', () => { }); // Set agent.connection_delay entry for every OS - cy.getByTestSubj('advancedPolicyButton').click(); - cy.getByTestSubj('advancedPolicyPanel') + cy.getByTestSubj(testSubjects.showHideButton).click(); + cy.getByTestSubj(testSubjects.settingsContainer) .children() .each(($child) => { const settingName = $child.find('label').text(); diff --git a/x-pack/plugins/security_solution/public/management/cypress/screens/policy_details.ts b/x-pack/plugins/security_solution/public/management/cypress/screens/policy_details.ts index de4649d3bab4c..30b0b392ff456 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/screens/policy_details.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/screens/policy_details.ts @@ -18,10 +18,13 @@ import type { PolicyConfig } from '../../../../common/endpoint/types'; import { request, loadPage } from '../tasks/common'; import { expectAndCloseSuccessToast } from '../tasks/toasts'; -export const visitPolicyDetailsPage = () => { - loadPage(APP_POLICIES_PATH); - - cy.getByTestSubj('policyNameCellLink').eq(0).click({ force: true }); +export const visitPolicyDetailsPage = (policyId?: string) => { + if (policyId) { + loadPage(`${APP_POLICIES_PATH}/${policyId}`); + } else { + cy.visit(APP_POLICIES_PATH); + cy.getByTestSubj('policyNameCellLink').eq(0).click({ force: true }); + } cy.getByTestSubj('policyDetailsPage').should('exist'); cy.get('#settings').should('exist'); // waiting for Policy Settings tab }; diff --git a/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.ts b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.ts new file mode 100644 index 0000000000000..78cf98c6b48bd --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy.ts @@ -0,0 +1,92 @@ +/* + * 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 { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { useQuery } from '@tanstack/react-query'; +import { packagePolicyRouteService } from '@kbn/fleet-plugin/common'; +import { + DefaultPolicyNotificationMessage, + DefaultPolicyRuleNotificationMessage, +} from '../../../../common/endpoint/models/policy_config'; +import type { GetPolicyResponse } from '../../pages/policy/types'; +import { useHttp } from '../../../common/lib/kibana'; +import type { PolicyData, PolicyConfig } from '../../../../common/endpoint/types'; +import type { ManifestSchema } from '../../../../common/endpoint/schema/manifest'; + +interface ApiDataResponse { + /** Data return from the Fleet API. Its the full integration policy (package policy) */ + item: PolicyData; + /** Endpoint policy settings from the data retrieved from fleet */ + settings: PolicyConfig; + /** Endpoint policy manifest info from the data retrieved from fleet */ + artifactManifest: ManifestSchema; +} + +type UseFetchEndpointPolicyResponse = UseQueryResult; + +/** + * Retrieve a single endpoint integration policy (details) + * @param policyId + * @param options + */ +export const useFetchEndpointPolicy = ( + policyId: string, + options: UseQueryOptions = {} +): UseFetchEndpointPolicyResponse => { + const http = useHttp(); + + return useQuery({ + queryKey: ['get-policy-details', policyId], + ...options, + queryFn: async () => { + const apiResponse = await http.get( + packagePolicyRouteService.getInfoPath(policyId) + ); + + applyDefaultsToPolicyIfNeeded(apiResponse.item); + + return { + item: apiResponse.item, + settings: apiResponse.item.inputs[0].config.policy.value, + artifactManifest: apiResponse.item.inputs[0].config.artifact_manifest.value, + }; + }, + }); +}; + +const applyDefaultsToPolicyIfNeeded = (policyItem: PolicyData): void => { + const settings = policyItem.inputs[0].config.policy.value; + + // sets default user notification message if policy config message is empty + if (settings.windows.popup.malware.message === '') { + settings.windows.popup.malware.message = DefaultPolicyNotificationMessage; + settings.mac.popup.malware.message = DefaultPolicyNotificationMessage; + settings.linux.popup.malware.message = DefaultPolicyNotificationMessage; + } + if (settings.windows.popup.ransomware.message === '') { + settings.windows.popup.ransomware.message = DefaultPolicyNotificationMessage; + } + if (settings.windows.popup.memory_protection.message === '') { + settings.windows.popup.memory_protection.message = DefaultPolicyRuleNotificationMessage; + } + if (settings.mac.popup.memory_protection.message === '') { + settings.mac.popup.memory_protection.message = DefaultPolicyRuleNotificationMessage; + } + if (settings.linux.popup.memory_protection.message === '') { + settings.linux.popup.memory_protection.message = DefaultPolicyRuleNotificationMessage; + } + if (settings.windows.popup.behavior_protection.message === '') { + settings.windows.popup.behavior_protection.message = DefaultPolicyRuleNotificationMessage; + } + if (settings.mac.popup.behavior_protection.message === '') { + settings.mac.popup.behavior_protection.message = DefaultPolicyRuleNotificationMessage; + } + if (settings.linux.popup.behavior_protection.message === '') { + settings.linux.popup.behavior_protection.message = DefaultPolicyRuleNotificationMessage; + } +}; diff --git a/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy_agent_summary.ts b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy_agent_summary.ts new file mode 100644 index 0000000000000..929665ee1a290 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/hooks/policy/use_fetch_endpoint_policy_agent_summary.ts @@ -0,0 +1,37 @@ +/* + * 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 { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import type { GetAgentStatusResponse } from '@kbn/fleet-plugin/common'; +import { useQuery } from '@tanstack/react-query'; +import { agentRouteService } from '@kbn/fleet-plugin/common'; +import { useHttp } from '../../../common/lib/kibana'; + +type EndpointPolicyAgentSummary = GetAgentStatusResponse['results']; + +export const useFetchAgentByAgentPolicySummary = ( + /** + * The Fleet Agent Policy ID (NOT the endpoint policy id) + */ + agentPolicyId: string, + options: UseQueryOptions = {} +): UseQueryResult => { + const http = useHttp(); + + return useQuery({ + queryKey: ['get-policy-agent-summary', agentPolicyId], + ...options, + queryFn: async () => { + return ( + await http.get(agentRouteService.getStatusPath(), { + query: { policyId: agentPolicyId }, + }) + ).results; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.ts b/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.ts new file mode 100644 index 0000000000000..a81ca3cb4f30d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/hooks/policy/use_update_endpoint_policy.ts @@ -0,0 +1,45 @@ +/* + * 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 { UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { useMutation } from '@tanstack/react-query'; +import { packagePolicyRouteService } from '@kbn/fleet-plugin/common'; +import { getPolicyDataForUpdate } from '../../../../common/endpoint/service/policy'; +import { useHttp } from '../../../common/lib/kibana'; +import type { PolicyData } from '../../../../common/endpoint/types'; +import type { UpdatePolicyResponse } from '../../pages/policy/types'; + +interface UpdateParams { + policy: PolicyData; +} + +type UseUpdateEndpointPolicyOptions = UseMutationOptions< + UpdatePolicyResponse, + IHttpFetchError, + UpdateParams +>; + +type UseUpdateEndpointPolicyResult = UseMutationResult< + UpdatePolicyResponse, + IHttpFetchError, + UpdateParams +>; + +export const useUpdateEndpointPolicy = ( + options?: UseUpdateEndpointPolicyOptions +): UseUpdateEndpointPolicyResult => { + const http = useHttp(); + + return useMutation(({ policy }) => { + const update = getPolicyDataForUpdate(policy); + + return http.put(packagePolicyRouteService.getUpdatePath(policy.id), { + body: JSON.stringify(update), + }); + }, options); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts deleted file mode 100644 index e4bf68d337bbf..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/policy_details_config.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { cloneDeep } from 'lodash'; -import type { UIPolicyConfig } from '../../../../../common/endpoint/types'; - -/** - * Returns cloned `configuration` with `value` set by the `keyPath`. - */ -export const setIn = - (a: UIPolicyConfig) => - (key: Key) => - (subKey: SubKey) => - (leafKey: LeafKey) => - (v: V): UIPolicyConfig => { - const c = cloneDeep(a); - c[key][subKey][leafKey] = v; - return c; - }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts index 97d6886a49d37..5eac0615cc090 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts @@ -186,14 +186,6 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel } ); -export const isAntivirusRegistrationEnabled = createSelector(policyConfig, (uiPolicyConfig) => { - return uiPolicyConfig.windows.antivirus_registration.enabled; -}); - -export const isCredentialHardeningEnabled = createSelector(policyConfig, (uiPolicyConfig) => { - return uiPolicyConfig.windows.attack_surface_reduction.credential_hardening.enabled; -}); - /** is there an api call in flight */ export const isLoading = (state: PolicyDetailsState) => state.isLoading; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/agents_summary.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/agents_summary.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/agents_summary.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/components/agents_summary.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx deleted file mode 100644 index 7d28619e17e6e..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx +++ /dev/null @@ -1,82 +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, { memo, useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; - -import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/selectors'; -import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; -import { ConfigForm } from '../config_form'; - -const TRANSLATIONS: Readonly<{ [K in 'title' | 'description' | 'label']: string }> = { - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type', - { - defaultMessage: 'Register as antivirus', - } - ), - description: i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', - { - defaultMessage: - 'Toggle on to register Elastic as an official Antivirus solution for Windows OS. ' + - 'This will also disable Windows Defender.', - } - ), - label: i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.toggle', - { - defaultMessage: 'Register as antivirus', - } - ), -}; - -export const AntivirusRegistrationForm = memo(() => { - const antivirusRegistrationEnabled = usePolicyDetailsSelector(isAntivirusRegistrationEnabled); - const dispatch = useDispatch(); - const showEditableFormFields = useShowEditableFormFields(); - - const handleSwitchChange = useCallback( - (event) => - dispatch({ - type: 'userChangedAntivirusRegistration', - payload: { - enabled: event.target.checked, - }, - }), - [dispatch] - ); - - return ( - - {TRANSLATIONS.description} - - - - ); -}); - -AntivirusRegistrationForm.displayName = 'AntivirusRegistrationForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/attack_surface_reduction_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/attack_surface_reduction_form/index.tsx deleted file mode 100644 index 235fef2aeee8d..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/attack_surface_reduction_form/index.tsx +++ /dev/null @@ -1,61 +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, { memo, useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import { i18n } from '@kbn/i18n'; -import { EuiSwitch } from '@elastic/eui'; - -import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { isCredentialHardeningEnabled } from '../../../store/policy_details/selectors'; -import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; -import { ConfigForm } from '../config_form'; - -const TRANSLATIONS: Readonly<{ [K in 'title' | 'label']: string }> = { - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.attackSurfaceReduction.type', - { - defaultMessage: 'Attack surface reduction', - } - ), - label: i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.credentialHardening.toggle', - { - defaultMessage: 'Credential hardening', - } - ), -}; - -export const AttackSurfaceReductionForm = memo(() => { - const credentialHardeningEnabled = usePolicyDetailsSelector(isCredentialHardeningEnabled); - const dispatch = useDispatch(); - const showEditableFormFields = useShowEditableFormFields(); - - const handleSwitchChange = useCallback( - (event) => - dispatch({ - type: 'userChangedCredentialHardening', - payload: { - enabled: event.target.checked, - }, - }), - [dispatch] - ); - - return ( - - - - ); -}); - -AttackSurfaceReductionForm.displayName = 'AttackSurfaceReductionForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx deleted file mode 100644 index 79e32cf2e3672..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx +++ /dev/null @@ -1,67 +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 { ThemeProvider } from 'styled-components'; -import { addDecorator, storiesOf } from '@storybook/react'; -import { euiLightVars } from '@kbn/ui-theme'; -import { EuiCheckbox, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; - -import { OperatingSystem } from '@kbn/securitysolution-utils'; - -import { ConfigForm } from '.'; - -addDecorator((storyFn) => ( - ({ eui: euiLightVars, darkMode: false })}>{storyFn()} -)); - -storiesOf('PolicyDetails/ConfigForm', module) - .add('One OS', () => { - return ( - - {'Some content'} - - ); - }) - .add('Multiple OSs', () => { - return ( - - {'Some content'} - - ); - }) - .add('Complex content', () => { - return ( - - - {'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore ' + - 'et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut ' + - 'aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum ' + - 'dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia ' + - 'deserunt mollit anim id est laborum.'} - - - {}} /> - - {}} /> - {}} /> - {}} /> - - ); - }) - .add('Right corner content', () => { - const toggle = {}} />; - - return ( - - {'Some content'} - - ); - }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx deleted file mode 100644 index a3a0f8c325b0e..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx +++ /dev/null @@ -1,197 +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, { useContext, useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiCheckbox, - EuiSpacer, - EuiText, - htmlIdGenerator, - EuiIconTip, - EuiBetaBadge, - EuiFlexItem, - EuiFlexGroup, -} from '@elastic/eui'; -import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { ThemeContext } from 'styled-components'; -import type { - PolicyOperatingSystem, - UIPolicyConfig, -} from '../../../../../../../common/endpoint/types'; -import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import { ConfigForm, ConfigFormHeading } from '../config_form'; - -const OPERATING_SYSTEM_TO_TEST_SUBJ: { [K in OperatingSystem]: string } = { - [OperatingSystem.WINDOWS]: 'Windows', - [OperatingSystem.LINUX]: 'Linux', - [OperatingSystem.MAC]: 'Mac', -}; - -interface OperatingSystemToOsMap { - [OperatingSystem.WINDOWS]: PolicyOperatingSystem.windows; - [OperatingSystem.LINUX]: PolicyOperatingSystem.linux; - [OperatingSystem.MAC]: PolicyOperatingSystem.mac; -} - -export type ProtectionField = - keyof UIPolicyConfig[OperatingSystemToOsMap[T]]['events']; - -export type EventFormSelection = { [K in ProtectionField]: boolean }; - -export interface EventFormOption { - name: string; - protectionField: ProtectionField; -} - -export interface SupplementalEventFormOption { - title?: string; - description?: string; - name: string; - protectionField: ProtectionField; - tooltipText?: string; - beta?: boolean; - indented?: boolean; - isDisabled?(policyConfig: UIPolicyConfig): boolean; -} - -export interface EventsFormProps { - os: T; - options: ReadonlyArray>; - selection: EventFormSelection; - onValueSelection: (value: ProtectionField, selected: boolean) => void; - supplementalOptions?: ReadonlyArray>; -} - -const InnerEventsForm = ({ - os, - options, - selection, - onValueSelection, - supplementalOptions, -}: EventsFormProps) => { - const showEditableFormFields = useShowEditableFormFields(); - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const theme = useContext(ThemeContext); - const countSelected = useCallback(() => { - const supplementalSelectionFields: string[] = supplementalOptions - ? supplementalOptions.map((value) => value.protectionField as string) - : []; - return Object.entries(selection).filter(([key, value]) => - !supplementalSelectionFields.includes(key) ? value : false - ).length; - }, [selection, supplementalOptions]); - - return ( - - {i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', - { - defaultMessage: '{selected} / {total} event collections enabled', - values: { - selected: countSelected(), - total: options.length, - }, - } - )} - - } - > - - {i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', { - defaultMessage: 'Events', - })} - - - {options.map(({ name, protectionField }) => { - return ( - onValueSelection(protectionField, event.target.checked)} - disabled={!showEditableFormFields} - /> - ); - })} - {supplementalOptions && - supplementalOptions.map( - ({ - title, - description, - name, - protectionField, - tooltipText, - beta, - indented, - isDisabled, - }) => { - return ( -
- {title && ( - <> - - {title} - - )} - {description && ( - <> - - - {description} - - - )} - - - - onValueSelection(protectionField, event.target.checked)} - disabled={ - !showEditableFormFields || - (isDisabled ? isDisabled(policyDetailsConfig) : false) - } - /> - - {tooltipText && ( - - - - )} - {beta && ( - - - - )} - -
- ); - } - )} -
- ); -}; - -InnerEventsForm.displayName = 'EventsForm'; - -export const EventsForm = React.memo(InnerEventsForm) as typeof InnerEventsForm; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts index 540484a710913..d8beeec8b9d60 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts @@ -7,4 +7,3 @@ export * from './policy_list'; export * from './policy_details'; -export * from './policy_advanced'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.test.tsx index 71bad39304740..ab6c61fef54a2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.test.tsx @@ -14,6 +14,8 @@ import type { AppContextTestRender } from '../../../../../../common/mock/endpoin import { EndpointPolicyEditExtension } from './endpoint_policy_edit_extension'; import { createFleetContextRendererMock } from '../mocks'; import { getUserPrivilegesMockDefaultValue } from '../../../../../../common/components/user_privileges/__mocks__'; +import { FleetPackagePolicyGenerator } from '../../../../../../../common/endpoint/data_generators/fleet_package_policy_generator'; +import { getPolicyDataForUpdate } from '../../../../../../../common/endpoint/service/policy'; jest.mock('../../../../../../common/components/user_privileges'); const useUserPrivilegesMock = useUserPrivileges as jest.Mock; @@ -29,12 +31,16 @@ describe('When displaying the EndpointPolicyEditExtension fleet UI extension', ( beforeEach(() => { const mockedTestContext = createFleetContextRendererMock(); + const policy = new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy({ + id: 'someid', + }); + const newPolicy = getPolicyDataForUpdate(policy); render = () => mockedTestContext.render( ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.tsx index a9037ef6c3bb2..fab55babae419 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension/endpoint_policy_edit_extension.tsx @@ -5,136 +5,68 @@ * 2.0. */ -import React, { memo, useEffect, useState } from 'react'; -import { EuiCallOut, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { memo, useCallback } from 'react'; +import { EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useDispatch } from 'react-redux'; -import type { - PackagePolicyEditExtensionComponentProps, - NewPackagePolicy, -} from '@kbn/fleet-plugin/public'; +import type { PackagePolicyEditExtensionComponentProps } from '@kbn/fleet-plugin/public'; +import { cloneDeep } from 'lodash'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; +import type { PolicySettingsFormProps } from '../../policy_settings_form/policy_settings_form'; +import type { NewPolicyData } from '../../../../../../../common/endpoint/types'; import { EndpointPolicyArtifactCards } from './components/endpoint_policy_artifact_cards'; -import { getPolicyDetailPath } from '../../../../../common/routing'; -import { PolicyDetailsForm } from '../../policy_details_form'; -import type { AppAction } from '../../../../../../common/store/actions'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import { - apiError, - policyDetails, - policyDetailsForUpdate, -} from '../../../store/policy_details/selectors'; +import { PolicySettingsForm } from '../../policy_settings_form'; /** * Exports Endpoint-specific package policy instructions * for use in the Ingest app create / edit package policy */ export const EndpointPolicyEditExtension = memo( - ({ policy, onChange }) => { - return ( - <> - - - - ); - } -); -EndpointPolicyEditExtension.displayName = 'EndpointPolicyEditExtension'; + ({ policy, onChange, newPolicy: _newPolicy }) => { + const policyUpdates = _newPolicy as NewPolicyData; + const endpointPolicySettings = policyUpdates.inputs[0].config.policy.value; + const { canAccessFleet } = useUserPrivileges().endpointPrivileges; -const WrappedPolicyDetailsForm = memo<{ - policyId: string; - onChange: PackagePolicyEditExtensionComponentProps['onChange']; -}>(({ policyId, onChange }) => { - const dispatch = useDispatch<(a: AppAction) => void>(); - const updatedPolicy = usePolicyDetailsSelector(policyDetailsForUpdate); - const endpointPolicyDetails = usePolicyDetailsSelector(policyDetails); - const endpointDetailsLoadingError = usePolicyDetailsSelector(apiError); - const [, setLastUpdatedPolicy] = useState(updatedPolicy); + const endpointPolicySettingsOnChangeHandler: PolicySettingsFormProps['onChange'] = useCallback( + ({ isValid, updatedPolicy }) => { + const newPolicyInputs = cloneDeep(policyUpdates.inputs); + newPolicyInputs[0].config.policy.value = updatedPolicy; - // When the form is initially displayed, trigger the Redux middleware which is based on - // the location information stored via the `userChangedUrl` action. - useEffect(() => { - dispatch({ - type: 'userChangedUrl', - payload: { - hash: '', - pathname: getPolicyDetailPath(policyId, ''), - search: '', + onChange({ + isValid, + updatedPolicy: { inputs: newPolicyInputs }, + }); }, - }); - - // When form is unloaded, reset the redux store - return () => { - dispatch({ - type: 'userChangedUrl', - payload: { - hash: '', - pathname: '/', - search: '', - }, - }); - }; - }, [dispatch, policyId]); + [onChange, policyUpdates.inputs] + ); - useEffect(() => { - // Currently, the `onChange` callback provided by the fleet UI extension is regenerated every - // time the policy data is updated, which means this will go into a continuous loop if we don't - // actually check to see if an update should be reported back to fleet - setLastUpdatedPolicy((prevState) => { - if (prevState === updatedPolicy) { - return prevState; - } + return ( + <> + +
+ - if (updatedPolicy) { - onChange({ - isValid: true, - // send up only the updated policy data which is stored in the `inputs` section. - // All other attributes (like name, id) are updated from the Fleet form, so we want to - // ensure we don't override it. - updatedPolicy: { - // Casting is needed due to the use of `Immutable<>` in our store data - inputs: updatedPolicy.inputs as unknown as NewPackagePolicy['inputs'], - }, - }); - } +
+ +
+ +
+
- return updatedPolicy; - }); - }, [onChange, updatedPolicy]); + - return ( -
- -
- -
- -
-
- - {endpointDetailsLoadingError ? ( - - } - iconType="warning" - color="warning" - data-test-subj="endpiontPolicySettingsLoadingError" - > - {endpointDetailsLoadingError.message} - - ) : !endpointPolicyDetails ? ( - - ) : ( - - )} -
-
- ); -}); -WrappedPolicyDetailsForm.displayName = 'WrappedPolicyDetailsForm'; +
+
+ + ); + } +); +EndpointPolicyEditExtension.displayName = 'EndpointPolicyEditExtension'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx deleted file mode 100644 index 35141c4cf6d8b..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_advanced.tsx +++ /dev/null @@ -1,213 +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, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import { - EuiCallOut, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiIconTip, - EuiPanel, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { cloneDeep } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { policyConfig } from '../store/policy_details/selectors'; -import { useShowEditableFormFields, usePolicyDetailsSelector } from './policy_hooks'; -import { AdvancedPolicySchema } from '../models/advanced_policy_schema'; - -function setValue(obj: Record, value: string, path: string[]) { - let newPolicyConfig = obj; - - // First set the value. - for (let i = 0; i < path.length - 1; i++) { - if (!newPolicyConfig[path[i]]) { - newPolicyConfig[path[i]] = {} as Record; - } - newPolicyConfig = newPolicyConfig[path[i]] as Record; - } - newPolicyConfig[path[path.length - 1]] = value; - - // Then, if the user is deleting the value, we need to ensure we clean up the config. - // We delete any sections that are empty, whether that be an empty string, empty object, or undefined. - if (value === '' || value === undefined) { - newPolicyConfig = obj; - for (let k = path.length; k >= 0; k--) { - const nextPath = path.slice(0, k); - for (let i = 0; i < nextPath.length - 1; i++) { - // Traverse and find the next section - newPolicyConfig = newPolicyConfig[nextPath[i]] as Record; - } - if ( - newPolicyConfig[nextPath[nextPath.length - 1]] === undefined || - newPolicyConfig[nextPath[nextPath.length - 1]] === '' || - Object.keys(newPolicyConfig[nextPath[nextPath.length - 1]] as object).length === 0 - ) { - // If we're looking at the `advanced` field, we leave it undefined as opposed to deleting it. - // This is because the UI looks for this field to begin rendering. - if (nextPath[nextPath.length - 1] === 'advanced') { - newPolicyConfig[nextPath[nextPath.length - 1]] = undefined; - // In all other cases, if field is empty, we'll delete it to clean up. - } else { - delete newPolicyConfig[nextPath[nextPath.length - 1]]; - } - newPolicyConfig = obj; - } else { - break; // We are looking at a non-empty section, so we can terminate. - } - } - } -} - -function getValue(obj: Record, path: string[]) { - let currentPolicyConfig = obj; - - for (let i = 0; i < path.length - 1; i++) { - if (currentPolicyConfig[path[i]]) { - currentPolicyConfig = currentPolicyConfig[path[i]] as Record; - } else { - return undefined; - } - } - return currentPolicyConfig[path[path.length - 1]]; -} -const calloutTitle = i18n.translate( - 'xpack.securitySolution.endpoint.policy.advanced.calloutTitle', - { - defaultMessage: 'Proceed with caution!', - } -); -const warningMessage = i18n.translate( - 'xpack.securitySolution.endpoint.policy.advanced.warningMessage', - { - defaultMessage: `This section contains policy values that support advanced use cases. If not configured - properly, these values can cause unpredictable behavior. Please consult documentation - carefully or contact support before editing these values.`, - } -); - -export const AdvancedPolicyForms = React.memo(({ isPlatinumPlus }: { isPlatinumPlus: boolean }) => { - return ( - <> - -

{warningMessage}

-
- - -

- -

-
- - {AdvancedPolicySchema.map((advancedField, index) => { - const configPath = advancedField.key.split('.'); - const failsPlatinumLicenseCheck = !isPlatinumPlus && advancedField.license === 'platinum'; - return ( - !failsPlatinumLicenseCheck && ( - - ) - ); - })} - - - ); -}); - -AdvancedPolicyForms.displayName = 'AdvancedPolicyForms'; - -const PolicyAdvanced = React.memo( - ({ - configPath, - firstSupportedVersion, - lastSupportedVersion, - documentation, - }: { - configPath: string[]; - firstSupportedVersion: string; - lastSupportedVersion?: string; - documentation: string; - }) => { - const showEditableFormFields = useShowEditableFormFields(); - const dispatch = useDispatch(); - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const onChange = useCallback( - (event) => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - setValue( - newPayload as unknown as Record, - event.target.value, - configPath - ); - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [dispatch, policyDetailsConfig, configPath] - ); - - const value = - policyDetailsConfig && - getValue(policyDetailsConfig as unknown as Record, configPath); - - return ( - <> - - {configPath.join('.')} - {documentation && ( - - - - )} - - } - labelAppend={ - - {lastSupportedVersion - ? `${firstSupportedVersion}-${lastSupportedVersion}` - : `${firstSupportedVersion}+`} - - } - > - - - - ); - } -); - -PolicyAdvanced.displayName = 'PolicyAdvanced'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 020ef18ed0d8b..58bb5189332fe 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -28,9 +28,6 @@ import { policyListApiPathHandlers } from '../store/test_mock_utils'; import { PolicyDetails } from './policy_details'; import { APP_UI_ID } from '../../../../../common/constants'; -jest.mock('./policy_forms/components/policy_form_layout', () => ({ - PolicyFormLayout: () => <>, -})); jest.mock('../../../../common/components/user_privileges'); const useUserPrivilegesMock = useUserPrivileges as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 04d9e14efa5b9..5adf40105255c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { usePolicyDetailsSelector } from './policy_hooks'; import { policyDetails, agentStatusSummary, apiError } from '../store/policy_details/selectors'; -import { AgentsSummary } from './agents_summary'; +import { AgentsSummary } from './components/agents_summary'; import { PolicyTabs } from './tabs'; import { AdministrationListPage } from '../../../components/administration_list_page'; import type { BackToExternalAppButtonProps } from '../../../components/back_to_external_app_button/back_to_external_app_button'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx deleted file mode 100644 index 61947bc5982f4..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx +++ /dev/null @@ -1,131 +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 { EuiButtonEmpty, EuiSkeletonText, EuiSpacer, EuiText } from '@elastic/eui'; -import React, { memo, useCallback, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { useUserPrivileges } from '../../../../common/components/user_privileges'; -import { MalwareProtections } from './policy_forms/protections/malware'; -import { MemoryProtection } from './policy_forms/protections/memory'; -import { BehaviorProtection } from './policy_forms/protections/behavior'; -import { LinuxEvents, MacEvents, WindowsEvents } from './policy_forms/events'; -import { AdvancedPolicyForms } from './policy_advanced'; -import { AntivirusRegistrationForm } from './components/antivirus_registration_form'; -import { AttackSurfaceReductionForm } from './components/attack_surface_reduction_form'; -import { Ransomware } from './policy_forms/protections/ransomware'; -import { LockedPolicyCard } from './policy_forms/locked_card'; -import { useLicense } from '../../../../common/hooks/use_license'; - -const LOCKED_CARD_RAMSOMWARE_TITLE = i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.ransomware', - { - defaultMessage: 'Ransomware', - } -); - -const LOCKED_CARD_MEMORY_TITLE = i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.memory', - { - defaultMessage: 'Memory Threat', - } -); - -const LOCKED_CARD_BEHAVIOR_TITLE = i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.behavior', - { - defaultMessage: 'Malicious Behavior', - } -); - -const LOCKED_CARD_ATTACK_SURFACE_REDUCTION = i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.attack_surface_reduction', - { - defaultMessage: 'Attack Surface Reduction', - } -); - -export const PolicyDetailsForm = memo(() => { - const [showAdvancedPolicy, setShowAdvancedPolicy] = useState(false); - const handleAdvancedPolicyClick = useCallback(() => { - setShowAdvancedPolicy(!showAdvancedPolicy); - }, [showAdvancedPolicy]); - const isPlatinumPlus = useLicense().isPlatinumPlus(); - const { loading: authzLoading } = useUserPrivileges().endpointPrivileges; - - if (authzLoading) { - return ; - } - - return ( - <> - -

- -

-
- - - - - {isPlatinumPlus ? : } - - {isPlatinumPlus ? ( - - ) : ( - - )} - - {isPlatinumPlus ? ( - - ) : ( - - )} - - {isPlatinumPlus ? ( - - ) : ( - - )} - - - -

- -

-
- - - - - - - - - - - - - - - - - {showAdvancedPolicy && } - - ); -}); -PolicyDetailsForm.displayName = 'PolicyDetailsForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx deleted file mode 100644 index 6d07b3784c6c2..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx +++ /dev/null @@ -1,470 +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 type { ReactWrapper } from 'enzyme'; -import { mount } from 'enzyme'; - -import { PolicyFormLayout } from './policy_form_layout'; -import '../../../../../../common/mock/match_media'; -import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; -import type { AppContextTestRender } from '../../../../../../common/mock/endpoint'; -import { - createAppRootMockRenderer, - resetReactDomCreatePortalMock, -} from '../../../../../../common/mock/endpoint'; -import { getPolicyDetailPath, getPoliciesPath } from '../../../../../common/routing'; -import { policyListApiPathHandlers } from '../../../store/test_mock_utils'; -import { licenseService } from '../../../../../../common/hooks/use_license'; -import { PACKAGE_POLICY_API_ROOT, AGENT_API_ROUTES } from '@kbn/fleet-plugin/common'; -import { useUserPrivileges as _useUserPrivileges } from '../../../../../../common/components/user_privileges'; -import { getUserPrivilegesMockDefaultValue } from '../../../../../../common/components/user_privileges/__mocks__'; - -jest.mock('../../../../../../common/hooks/use_license'); -jest.mock('../../../../../../common/components/user_privileges'); - -const useUserPrivilegesMock = _useUserPrivileges as jest.Mock; - -describe('Policy Form Layout', () => { - type FindReactWrapperResponse = ReturnType['find']>; - - const policyDetailsPathUrl = getPolicyDetailPath('1'); - const policyListPath = getPoliciesPath(); - const sleep = (ms = 100) => new Promise((wakeup) => setTimeout(wakeup, ms)); - const generator = new EndpointDocGenerator(); - let history: AppContextTestRender['history']; - let coreStart: AppContextTestRender['coreStart']; - let http: typeof coreStart.http; - let render: (ui: Parameters[0]) => ReturnType; - let policyPackagePolicy: ReturnType; - let policyFormLayoutView: ReturnType; - - beforeAll(() => resetReactDomCreatePortalMock()); - - beforeEach(() => { - const appContextMockRenderer = createAppRootMockRenderer(); - const AppWrapper = appContextMockRenderer.AppWrapper; - - ({ history, coreStart } = appContextMockRenderer); - render = (ui) => mount(ui, { wrappingComponent: AppWrapper }); - http = coreStart.http; - }); - - afterEach(() => { - if (policyFormLayoutView) { - policyFormLayoutView.unmount(); - } - jest.clearAllMocks(); - }); - - describe('when displayed with valid id', () => { - let asyncActions: Promise = Promise.resolve(); - - beforeEach(async () => { - policyPackagePolicy = generator.generatePolicyPackagePolicy(); - policyPackagePolicy.id = '1'; - - const policyListApiHandlers = policyListApiPathHandlers(); - - http.get.mockImplementation((...args) => { - const [path] = args; - if (typeof path === 'string') { - // GET datasouce - if (path === `${PACKAGE_POLICY_API_ROOT}/1`) { - asyncActions = asyncActions.then(async (): Promise => sleep()); - return Promise.resolve({ - item: policyPackagePolicy, - success: true, - }); - } - - // GET Agent status for agent policy - if (path === `${AGENT_API_ROUTES.STATUS_PATTERN}`) { - asyncActions = asyncActions.then(async () => sleep()); - return Promise.resolve({ - results: { events: 0, total: 5, online: 3, error: 1, offline: 1 }, - success: true, - }); - } - - // Get package data - // Used in tests that route back to the list - if (policyListApiHandlers[path]) { - asyncActions = asyncActions.then(async () => sleep()); - return Promise.resolve(policyListApiHandlers[path]()); - } - } - - return Promise.reject(new Error(`unknown API call (not MOCKED): ${path}`)); - }); - history.push(policyDetailsPathUrl); - policyFormLayoutView = render(); - - await asyncActions; - policyFormLayoutView.update(); - }); - - it('should NOT display timeline', async () => { - expect(policyFormLayoutView.find('flyoutOverlay')).toHaveLength(0); - }); - - it('should display cancel button', async () => { - const cancelbutton = policyFormLayoutView.find( - 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' - ); - expect(cancelbutton).toHaveLength(1); - expect(cancelbutton.text()).toEqual('Cancel'); - }); - it('should redirect to policy list when cancel button is clicked', async () => { - const cancelbutton = policyFormLayoutView.find( - 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' - ); - expect(history.location.pathname).toEqual(policyDetailsPathUrl); - cancelbutton.simulate('click', { button: 0 }); - const navigateToAppMockedCalls = coreStart.application.navigateToApp.mock.calls; - expect(navigateToAppMockedCalls[navigateToAppMockedCalls.length - 1]).toEqual([ - 'securitySolutionUI', - { path: policyListPath }, - ]); - }); - it('should display save button', async () => { - const saveButton = policyFormLayoutView.find( - 'EuiButton[data-test-subj="policyDetailsSaveButton"]' - ); - expect(saveButton).toHaveLength(1); - expect(saveButton.text()).toEqual('Save'); - }); - it('should display beta badge', async () => { - const saveButton = policyFormLayoutView.find('EuiBetaBadge'); - expect(saveButton).toHaveLength(1); - expect(saveButton.text()).toEqual('beta'); - }); - - it('should display minimum Agent version number for User Notification', async () => { - const minVersionsMap = [ - ['malware', '7.11'], - ['ransomware', '7.12'], - ['behavior', '7.15'], - ['memory', '7.15'], - ]; - - for (const [protection, minVersion] of minVersionsMap) { - expect( - policyFormLayoutView - .find(`EuiPanel[data-test-subj="${protection}ProtectionsForm"]`) - .find('EuiText[data-test-subj="policySupportedVersions"]') - .text() - ).toEqual(`Agent version ${minVersion}+`); - } - }); - - it('"Register as antivirus" should be only available for Windows', () => { - const antivirusRegistrationFormTextContent = policyFormLayoutView - .find('EuiPanel[data-test-subj="antivirusRegistrationForm"]') - .text(); - - expect(antivirusRegistrationFormTextContent).toContain('Windows'); - expect(antivirusRegistrationFormTextContent).not.toContain('Linux'); - expect(antivirusRegistrationFormTextContent).not.toContain('Mac'); - - expect(antivirusRegistrationFormTextContent).toContain( - 'Toggle on to register Elastic as an official Antivirus solution for Windows OS. This will also disable Windows Defender.' - ); - }); - - describe('Advanced settings', () => { - let showHideAdvancedSettingsButton: ReactWrapper; - - beforeEach(() => { - showHideAdvancedSettingsButton = policyFormLayoutView.find( - 'EuiButtonEmpty[data-test-subj="advancedPolicyButton"]' - ); - }); - - it('should display "Show advanced settings" button, and hide advanced options on default', () => { - expect(showHideAdvancedSettingsButton.text()).toEqual('Show advanced settings'); - expect( - policyFormLayoutView.find('EuiPanel[data-test-subj="advancedPolicyPanel"]').length - ).toEqual(0); - }); - - it('clicking on "Show/Hide advanced settings" should show/hide advanced settings', () => { - showHideAdvancedSettingsButton.simulate('click'); - - expect(showHideAdvancedSettingsButton.text()).toEqual('Hide advanced settings'); - expect( - policyFormLayoutView.find('EuiPanel[data-test-subj="advancedPolicyPanel"]').length - ).toEqual(1); - - showHideAdvancedSettingsButton.simulate('click'); - - expect( - policyFormLayoutView.find('EuiPanel[data-test-subj="advancedPolicyPanel"]').length - ).toEqual(0); - }); - - it('should display a warning message', () => { - showHideAdvancedSettingsButton.simulate('click'); - - expect( - policyFormLayoutView.find('div[data-test-subj="policyAdvancedSettingsWarning"]').text() - ).toContain( - `This section contains policy values that support advanced use cases. If not configured - properly, these values can cause unpredictable behavior. Please consult documentation - carefully or contact support before editing these values.` - ); - }); - - it('every row should contain a tooltip', () => { - showHideAdvancedSettingsButton.simulate('click'); - - policyFormLayoutView - .find('EuiPanel[data-test-subj="advancedPolicyPanel"]') - .find('EuiFormRow') - .forEach((row) => { - expect(row.find('EuiIconTip').length).toEqual(1); - }); - }); - }); - - describe('when the save button is clicked', () => { - let saveButton: FindReactWrapperResponse; - let confirmModal: FindReactWrapperResponse; - let modalCancelButton: FindReactWrapperResponse; - let modalConfirmButton: FindReactWrapperResponse; - - beforeEach(async () => { - await asyncActions; - policyFormLayoutView.update(); - saveButton = policyFormLayoutView.find('button[data-test-subj="policyDetailsSaveButton"]'); - saveButton.simulate('click'); - policyFormLayoutView.update(); - confirmModal = policyFormLayoutView.find( - 'EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]' - ); - modalCancelButton = confirmModal.find('button[data-test-subj="confirmModalCancelButton"]'); - modalConfirmButton = confirmModal.find( - 'button[data-test-subj="confirmModalConfirmButton"]' - ); - http.put.mockImplementation((...args) => { - asyncActions = asyncActions.then(async () => sleep()); - const [path] = args; - if (typeof path === 'string') { - if (path === `${PACKAGE_POLICY_API_ROOT}/1`) { - return Promise.resolve({ - item: policyPackagePolicy, - success: true, - }); - } - } - - return Promise.reject(new Error('unknown PUT path!')); - }); - }); - - it('should show a modal confirmation', () => { - expect(confirmModal).toHaveLength(1); - expect( - confirmModal.find('[data-test-subj="confirmModalTitleText"]').first().text() - ).toEqual('Save and deploy changes'); - expect(modalCancelButton.text()).toEqual('Cancel'); - expect(modalConfirmButton.text()).toEqual('Save and deploy changes'); - }); - it('should show info callout if policy is in use', () => { - const warningCallout = confirmModal.find( - 'EuiCallOut[data-test-subj="policyDetailsWarningCallout"]' - ); - expect(warningCallout).toHaveLength(1); - expect(warningCallout.text()).toEqual( - 'This action will update 5 endpointsSaving these changes will apply updates to all endpoints assigned to this agent policy.' - ); - }); - it('should close dialog if cancel button is clicked', () => { - modalCancelButton.simulate('click'); - expect( - policyFormLayoutView.find('EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]') - ).toHaveLength(0); - }); - it('should update policy and show success notification when confirm button is clicked', async () => { - modalConfirmButton.simulate('click'); - policyFormLayoutView.update(); - // Modal should be closed - expect( - policyFormLayoutView.find('EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]') - ).toHaveLength(0); - - // API should be called - await asyncActions; - expect(http.put.mock.calls[0][0]).toEqual(`${PACKAGE_POLICY_API_ROOT}/1`); - policyFormLayoutView.update(); - - // Toast notification should be shown - const toastAddMock = coreStart.notifications.toasts.addSuccess.mock; - expect(toastAddMock.calls).toHaveLength(1); - expect(toastAddMock.calls[0][0]).toMatchObject({ - title: 'Success!', - text: expect.any(Function), - }); - }); - it('should show an error notification toast if update fails', async () => { - policyPackagePolicy.id = 'invalid'; - modalConfirmButton.simulate('click'); - - await asyncActions; - policyFormLayoutView.update(); - - // Toast notification should be shown - const toastAddMock = coreStart.notifications.toasts.addDanger.mock; - expect(toastAddMock.calls).toHaveLength(1); - expect(toastAddMock.calls[0][0]).toMatchObject({ - title: 'Failed!', - text: expect.any(String), - }); - }); - }); - - describe('when the subscription tier is platinum or higher', () => { - beforeEach(() => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); - policyFormLayoutView = render(); - }); - - it('malware popup, message customization options and tooltip are shown', () => { - // use query for finding stuff, if it doesn't find it, just returns null - const userNotificationCheckbox = policyFormLayoutView.find( - 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' - ); - const userNotificationCustomMessageTextArea = policyFormLayoutView.find( - 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' - ); - const tooltip = policyFormLayoutView.find('EuiIconTip[data-test-subj="malwareTooltip"]'); - expect(userNotificationCheckbox).toHaveLength(1); - expect(userNotificationCustomMessageTextArea).toHaveLength(1); - expect(tooltip).toHaveLength(1); - }); - - it('memory protection card and user notification checkbox are shown', () => { - const memory = policyFormLayoutView.find( - 'EuiPanel[data-test-subj="memoryProtectionsForm"]' - ); - const userNotificationCheckbox = policyFormLayoutView.find( - 'EuiCheckbox[data-test-subj="memory_protectionUserNotificationCheckbox"]' - ); - - expect(memory).toHaveLength(1); - expect(userNotificationCheckbox).toHaveLength(1); - }); - - it('behavior protection card and user notification checkbox are shown', () => { - const behavior = policyFormLayoutView.find( - 'EuiPanel[data-test-subj="behaviorProtectionsForm"]' - ); - const userNotificationCheckbox = policyFormLayoutView.find( - 'EuiCheckbox[data-test-subj="behavior_protectionUserNotificationCheckbox"]' - ); - - expect(behavior).toHaveLength(1); - expect(userNotificationCheckbox).toHaveLength(1); - }); - - it('ransomware card is shown', () => { - const ransomware = policyFormLayoutView.find( - 'EuiPanel[data-test-subj="ransomwareProtectionsForm"]' - ); - expect(ransomware).toHaveLength(1); - }); - }); - - describe('when the subscription tier is gold or lower', () => { - beforeEach(() => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); - policyFormLayoutView = render(); - }); - - it('malware popup, message customization options, and tooltip are hidden', () => { - const userNotificationCheckbox = policyFormLayoutView.find( - 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' - ); - const userNotificationCustomMessageTextArea = policyFormLayoutView.find( - 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' - ); - const tooltip = policyFormLayoutView.find('EuiIconTip[data-test-subj="malwareTooltip"]'); - expect(userNotificationCheckbox).toHaveLength(0); - expect(userNotificationCustomMessageTextArea).toHaveLength(0); - expect(tooltip).toHaveLength(0); - }); - - it('memory protection card, and user notification checkbox are hidden', () => { - const memory = policyFormLayoutView.find( - 'EuiPanel[data-test-subj="memoryProtectionsForm"]' - ); - expect(memory).toHaveLength(0); - const userNotificationCheckbox = policyFormLayoutView.find( - 'EuiCheckbox[data-test-subj="memoryUserNotificationCheckbox"]' - ); - expect(userNotificationCheckbox).toHaveLength(0); - }); - - it('ransomware card is hidden', () => { - const ransomware = policyFormLayoutView.find( - 'EuiPanel[data-test-subj="ransomwareProtectionsForm"]' - ); - expect(ransomware).toHaveLength(0); - }); - - it('shows the locked card in place of paid features', () => { - const lockedCard = policyFormLayoutView.find('EuiCard[data-test-subj="lockedPolicyCard"]'); - expect(lockedCard).toHaveLength(4); - }); - - it('locked card has "Upgrade now" link to cloud server', () => { - const upgradeLinks = policyFormLayoutView.find( - 'EuiLink[data-test-subj="upgradeNowCloudDeploymentLink"]' - ); - upgradeLinks.forEach((link) => { - expect(link.prop('href')).toEqual('https://www.elastic.co/cloud/'); - }); - }); - }); - - describe('and user has only READ privilege', () => { - beforeEach(() => { - const mockedPrivileges = getUserPrivilegesMockDefaultValue(); - mockedPrivileges.endpointPrivileges.canWritePolicyManagement = false; - mockedPrivileges.endpointPrivileges.canAccessFleet = false; - - useUserPrivilegesMock.mockReturnValue(mockedPrivileges); - - policyFormLayoutView = render(); - }); - - afterEach(() => { - useUserPrivilegesMock.mockImplementation(getUserPrivilegesMockDefaultValue); - }); - - it('should not display the Save button', () => { - expect( - policyFormLayoutView.find('EuiButton[data-test-subj="policyDetailsSaveButton"]') - ).toHaveLength(0); - }); - - it('should display all form controls as disabled', () => { - policyFormLayoutView - .find('button[data-test-subj="advancedPolicyButton"]') - .simulate('click'); - - const inputElements = policyFormLayoutView.find('input'); - - expect(inputElements.length).toBeGreaterThan(0); - - inputElements.forEach((element) => { - expect(element.prop('disabled')).toBe(true); - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx deleted file mode 100644 index ec69dc8115b02..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_radio.tsx +++ /dev/null @@ -1,98 +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, { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; -import { cloneDeep } from 'lodash'; -import { htmlIdGenerator, EuiRadio } from '@elastic/eui'; -import type { ImmutableArray, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; -import { ProtectionModes } from '../../../../../../../common/endpoint/types'; -import type { MacPolicyProtection, LinuxPolicyProtection, PolicyProtection } from '../../../types'; -import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import type { AppAction } from '../../../../../../common/store/actions'; -import { useLicense } from '../../../../../../common/hooks/use_license'; - -export const ProtectionRadio = React.memo( - ({ - protection, - protectionMode, - osList, - label, - }: { - protection: PolicyProtection; - protectionMode: ProtectionModes; - osList: ImmutableArray>; - label: string; - }) => { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const dispatch = useDispatch<(action: AppAction) => void>(); - const radioButtonId = useMemo(() => htmlIdGenerator()(), []); - const selected = policyDetailsConfig && policyDetailsConfig.windows[protection].mode; - const isPlatinumPlus = useLicense().isPlatinumPlus(); - const showEditableFormFields = useShowEditableFormFields(); - - const handleRadioChange = useCallback(() => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - for (const os of osList) { - if (os === 'windows') { - newPayload[os][protection].mode = protectionMode; - } else if (os === 'mac') { - newPayload[os][protection as MacPolicyProtection].mode = protectionMode; - } else if (os === 'linux') { - newPayload[os][protection as LinuxPolicyProtection].mode = protectionMode; - } - if (isPlatinumPlus) { - if (os === 'windows') { - if (protectionMode === ProtectionModes.prevent) { - newPayload[os].popup[protection].enabled = true; - } else { - newPayload[os].popup[protection].enabled = false; - } - } else if (os === 'mac') { - if (protectionMode === ProtectionModes.prevent) { - newPayload[os].popup[protection as MacPolicyProtection].enabled = true; - } else { - newPayload[os].popup[protection as MacPolicyProtection].enabled = false; - } - } else if (os === 'linux') { - if (protectionMode === ProtectionModes.prevent) { - newPayload[os].popup[protection as LinuxPolicyProtection].enabled = true; - } else { - newPayload[os].popup[protection as LinuxPolicyProtection].enabled = false; - } - } - } - } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, [dispatch, protectionMode, policyDetailsConfig, isPlatinumPlus, osList, protection]); - - /** - * Passing an arbitrary id because EuiRadio - * requires an id if label is passed - */ - - return ( - - ); - } -); - -ProtectionRadio.displayName = 'ProtectionRadio'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx deleted file mode 100644 index 20536a1905382..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx +++ /dev/null @@ -1,134 +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, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import { i18n } from '@kbn/i18n'; -import { EuiSwitch } from '@elastic/eui'; -import { cloneDeep } from 'lodash'; -import { useLicense } from '../../../../../../common/hooks/use_license'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; -import type { AppAction } from '../../../../../../common/store/actions'; -import type { - ImmutableArray, - UIPolicyConfig, - AdditionalOnSwitchChangeParams, -} from '../../../../../../../common/endpoint/types'; -import { ProtectionModes } from '../../../../../../../common/endpoint/types'; -import type { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types'; - -export const ProtectionSwitch = React.memo( - ({ - protection, - protectionLabel, - osList, - additionalOnSwitchChange, - }: { - protection: PolicyProtection; - protectionLabel?: string; - osList: ImmutableArray>; - additionalOnSwitchChange?: ({ - value, - policyConfigData, - protectionOsList, - }: AdditionalOnSwitchChangeParams) => UIPolicyConfig; - }) => { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const isPlatinumPlus = useLicense().isPlatinumPlus(); - const showEditableFormFields = useShowEditableFormFields(); - const dispatch = useDispatch<(action: AppAction) => void>(); - const selected = policyDetailsConfig && policyDetailsConfig.windows[protection].mode; - - const handleSwitchChange = useCallback( - (event) => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - if (event.target.checked === false) { - for (const os of osList) { - if (os === 'windows') { - newPayload[os][protection].mode = ProtectionModes.off; - } else if (os === 'mac') { - newPayload[os][protection as MacPolicyProtection].mode = ProtectionModes.off; - } else if (os === 'linux') { - newPayload[os][protection as LinuxPolicyProtection].mode = ProtectionModes.off; - } - if (isPlatinumPlus) { - if (os === 'windows') { - newPayload[os].popup[protection].enabled = event.target.checked; - } else if (os === 'mac') { - newPayload[os].popup[protection as MacPolicyProtection].enabled = - event.target.checked; - } else if (os === 'linux') { - newPayload[os].popup[protection as LinuxPolicyProtection].enabled = - event.target.checked; - } - } - } - } else { - for (const os of osList) { - if (os === 'windows') { - newPayload[os][protection].mode = ProtectionModes.prevent; - } else if (os === 'mac') { - newPayload[os][protection as MacPolicyProtection].mode = ProtectionModes.prevent; - } else if (os === 'linux') { - newPayload[os][protection as LinuxPolicyProtection].mode = ProtectionModes.prevent; - } - if (isPlatinumPlus) { - if (os === 'windows') { - newPayload[os].popup[protection].enabled = event.target.checked; - } else if (os === 'mac') { - newPayload[os].popup[protection as MacPolicyProtection].enabled = - event.target.checked; - } else if (os === 'linux') { - newPayload[os].popup[protection as LinuxPolicyProtection].enabled = - event.target.checked; - } - } - } - } - if (additionalOnSwitchChange) { - dispatch({ - type: 'userChangedPolicyConfig', - payload: { - policyConfig: additionalOnSwitchChange({ - value: event.target.checked, - policyConfigData: newPayload, - protectionOsList: osList, - }), - }, - }); - } else { - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - } - }, - [dispatch, policyDetailsConfig, isPlatinumPlus, protection, osList, additionalOnSwitchChange] - ); - - return ( - - ); - } -); - -ProtectionSwitch.displayName = 'ProtectionSwitch'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx deleted file mode 100644 index ce4d86e037899..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx +++ /dev/null @@ -1,86 +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, { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiSpacer, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import type { - Immutable, - ImmutableArray, - UIPolicyConfig, -} from '../../../../../../../common/endpoint/types'; -import { ProtectionModes } from '../../../../../../../common/endpoint/types'; -import type { PolicyProtection } from '../../../types'; -import { ConfigFormHeading } from '../../components/config_form'; -import { ProtectionRadio } from './protection_radio'; - -export const RadioButtons = React.memo( - ({ - protection, - osList, - }: { - protection: PolicyProtection; - osList: ImmutableArray>; - }) => { - const radios: Immutable< - Array<{ - id: ProtectionModes; - label: string; - }> - > = useMemo(() => { - return [ - { - id: ProtectionModes.detect, - label: i18n.translate('xpack.securitySolution.endpoint.policy.details.detect', { - defaultMessage: 'Detect', - }), - }, - { - id: ProtectionModes.prevent, - label: i18n.translate('xpack.securitySolution.endpoint.policy.details.prevent', { - defaultMessage: 'Prevent', - }), - }, - ]; - }, []); - - return ( - <> - - - - - - - - - - - - - - ); - } -); - -RadioButtons.displayName = 'RadioButtons'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/supported_version.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/supported_version.tsx deleted file mode 100644 index b3cf322f70fac..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/supported_version.tsx +++ /dev/null @@ -1,30 +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 { FormattedMessage } from '@kbn/i18n-react'; -import { EuiText } from '@elastic/eui'; -import { popupVersionsMap } from '../protections/popup_options_to_versions'; - -export const SupportedVersionNotice = ({ optionName }: { optionName: string }) => { - const version = popupVersionsMap.get(optionName); - if (!version) { - return null; - } - - return ( - - - - - - ); -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx deleted file mode 100644 index baecfef415a02..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/user_notification.tsx +++ /dev/null @@ -1,210 +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, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { cloneDeep } from 'lodash'; -import { - EuiSpacer, - EuiFlexItem, - EuiFlexGroup, - EuiCheckbox, - EuiIconTip, - EuiText, - EuiTextArea, -} from '@elastic/eui'; -import type { ImmutableArray, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; -import { ProtectionModes } from '../../../../../../../common/endpoint/types'; -import type { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types'; -import { ConfigFormHeading } from '../../components/config_form'; -import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import type { AppAction } from '../../../../../../common/store/actions'; -import { SupportedVersionNotice } from './supported_version'; - -export const UserNotification = React.memo( - ({ - protection, - osList, - }: { - protection: PolicyProtection; - osList: ImmutableArray>; - }) => { - const showEditableFormFields = useShowEditableFormFields(); - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const dispatch = useDispatch<(action: AppAction) => void>(); - const selected = policyDetailsConfig && policyDetailsConfig.windows[protection].mode; - const userNotificationSelected = - policyDetailsConfig && policyDetailsConfig.windows.popup[protection].enabled; - const userNotificationMessage = - policyDetailsConfig && policyDetailsConfig.windows.popup[protection].message; - - const handleUserNotificationCheckbox = useCallback( - (event) => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - for (const os of osList) { - if (os === 'windows') { - newPayload[os].popup[protection].enabled = event.target.checked; - } else if (os === 'mac') { - newPayload[os].popup[protection as MacPolicyProtection].enabled = - event.target.checked; - } else if (os === 'linux') { - newPayload[os].popup[protection as LinuxPolicyProtection].enabled = - event.target.checked; - } - } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [policyDetailsConfig, dispatch, protection, osList] - ); - - const handleCustomUserNotification = useCallback( - (event) => { - if (policyDetailsConfig) { - const newPayload = cloneDeep(policyDetailsConfig); - for (const os of osList) { - if (os === 'windows') { - newPayload[os].popup[protection].message = event.target.value; - } else if (os === 'mac') { - newPayload[os].popup[protection as MacPolicyProtection].message = event.target.value; - } else if (os === 'linux') { - newPayload[os].popup[protection as LinuxPolicyProtection].message = - event.target.value; - } - } - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [policyDetailsConfig, dispatch, protection, osList] - ); - - const tooltipProtectionText = (protectionType: PolicyProtection) => { - if (protectionType === 'memory_protection') { - return i18n.translate( - 'xpack.securitySolution.endpoint.policyDetail.memoryProtectionTooltip', - { - defaultMessage: 'memory threat', - } - ); - } else if (protectionType === 'behavior_protection') { - return i18n.translate( - 'xpack.securitySolution.endpoint.policyDetail.behaviorProtectionTooltip', - { - defaultMessage: 'malicious behavior', - } - ); - } else { - return protectionType; - } - }; - - const tooltipBracketText = (protectionType: PolicyProtection) => { - if (protectionType === 'memory_protection' || protection === 'behavior_protection') { - return i18n.translate('xpack.securitySolution.endpoint.policyDetail.rule', { - defaultMessage: 'rule', - }); - } else { - return i18n.translate('xpack.securitySolution.endpoint.policyDetail.filename', { - defaultMessage: 'filename', - }); - } - }; - - return ( - <> - - - - - - - - {userNotificationSelected && ( - <> - - - - -

- -

-
-
- - - - - - - } - /> - -
- - - - )} - - ); - } -); - -UserNotification.displayName = 'UserNotification'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx deleted file mode 100644 index 5d503a9667d39..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx +++ /dev/null @@ -1,93 +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, { memo } from 'react'; -import { - EuiCard, - EuiIcon, - EuiTextColor, - EuiLink, - EuiFlexGroup, - EuiFlexItem, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; - -const LockedPolicyDiv = styled.div` - .euiCard__betaBadgeWrapper { - .euiCard__betaBadge { - width: auto; - } - } - .lockedCardDescription { - padding: 0 33.3%; - } -`; - -export const LockedPolicyCard = memo(({ title }: { title: string }) => { - return ( - - } - title={ -

- {title} -

- } - description={false} - > - - - -

- - - -

-
- -

- - - - ), - }} - /> -

-
-
-
-
-
- ); -}); -LockedPolicyCard.displayName = 'LockedPolicyCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx deleted file mode 100644 index 1e9efefe4ac60..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx +++ /dev/null @@ -1,74 +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 { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { OperatingSystem } from '@kbn/securitysolution-utils'; -import type { Immutable } from '../../../../../../../common/endpoint/types'; -import { PolicyOperatingSystem } from '../../../../../../../common/endpoint/types'; -import type { BehaviorProtectionOSes } from '../../../types'; -import { ConfigForm } from '../../components/config_form'; -import { RadioButtons } from '../components/radio_buttons'; -import { UserNotification } from '../components/user_notification'; -import { ProtectionSwitch } from '../components/protection_switch'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; -import { SecurityPageName } from '../../../../../../app/types'; - -/** The Behavior Protections form for policy details - * which will configure for all relevant OSes. - */ -export const BehaviorProtection = React.memo(() => { - const OSes: Immutable = [ - PolicyOperatingSystem.windows, - PolicyOperatingSystem.mac, - PolicyOperatingSystem.linux, - ]; - const protection = 'behavior_protection'; - const protectionLabel = i18n.translate( - 'xpack.securitySolution.endpoint.policy.protections.behavior', - { - defaultMessage: 'Malicious behavior protections', - } - ); - return ( - - } - > - - - - - - - - ), - }} - /> - - - ); -}); - -BehaviorProtection.displayName = 'BehaviorProtection'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx deleted file mode 100644 index a9782c8a56a02..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ /dev/null @@ -1,162 +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, { useCallback, useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiCallOut, - EuiSpacer, - EuiSwitch, - EuiFlexGroup, - EuiFlexItem, - EuiIconTip, -} from '@elastic/eui'; -import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { useDispatch } from 'react-redux'; -import { cloneDeep } from 'lodash'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { SecurityPageName } from '../../../../../../app/types'; -import type { - Immutable, - AdditionalOnSwitchChangeParams, - UIPolicyConfig, -} from '../../../../../../../common/endpoint/types'; -import { PolicyOperatingSystem } from '../../../../../../../common/endpoint/types'; -import type { MalwareProtectionOSes } from '../../../types'; -import { ConfigForm } from '../../components/config_form'; -import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; -import { useLicense } from '../../../../../../common/hooks/use_license'; -import { RadioButtons } from '../components/radio_buttons'; -import { UserNotification } from '../components/user_notification'; -import { ProtectionSwitch } from '../components/protection_switch'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; -import type { AppAction } from '../../../../../../common/store/actions'; - -/** The Malware Protections form for policy details - * which will configure for all relevant OSes. - */ -export const MalwareProtections = React.memo(() => { - const OSes: Immutable = useMemo( - () => [PolicyOperatingSystem.windows, PolicyOperatingSystem.mac, PolicyOperatingSystem.linux], - [] - ); - const protection = 'malware'; - const protectionLabel = i18n.translate( - 'xpack.securitySolution.endpoint.policy.protections.malware', - { - defaultMessage: 'Malware protections', - } - ); - const blocklistLabel = i18n.translate( - 'xpack.securitySolution.endpoint.policy.protections.blocklist', - { - defaultMessage: 'Blocklist enabled', - } - ); - const showEditableFormFields = useShowEditableFormFields(); - const isPlatinumPlus = useLicense().isPlatinumPlus(); - const dispatch = useDispatch<(action: AppAction) => void>(); - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - - const blocklistUpdate = ({ - value, - policyConfigData, - protectionOsList, - }: AdditionalOnSwitchChangeParams): UIPolicyConfig => { - const newPayload: UIPolicyConfig = cloneDeep(policyConfigData); - for (const os of protectionOsList) { - newPayload[os][protection].blocklist = value; - } - - return newPayload; - }; - - const handleBlocklistSwitchChange = useCallback( - (event) => { - if (policyDetailsConfig) { - const newPayload = blocklistUpdate({ - value: event.target.checked, - policyConfigData: cloneDeep(policyDetailsConfig), - protectionOsList: OSes, - }); - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: newPayload }, - }); - } - }, - [dispatch, OSes, policyDetailsConfig] - ); - - return ( - - } - > - - - - - - - - - - - } - /> - - - {isPlatinumPlus && } - - - - - - ), - }} - /> - - - ); -}); - -MalwareProtections.displayName = 'MalwareProtections'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx deleted file mode 100644 index 5f9fec17d1749..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx +++ /dev/null @@ -1,74 +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 { FormattedMessage } from '@kbn/i18n-react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { SecurityPageName } from '../../../../../../app/types'; -import type { Immutable } from '../../../../../../../common/endpoint/types'; -import { PolicyOperatingSystem } from '../../../../../../../common/endpoint/types'; -import type { MemoryProtectionOSes } from '../../../types'; -import { ConfigForm } from '../../components/config_form'; -import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; -import { RadioButtons } from '../components/radio_buttons'; -import { UserNotification } from '../components/user_notification'; -import { ProtectionSwitch } from '../components/protection_switch'; - -/** The Memory Protections form for policy details - * which will configure for all relevant OSes. - */ -export const MemoryProtection = React.memo(() => { - const OSes: Immutable = [ - PolicyOperatingSystem.windows, - PolicyOperatingSystem.mac, - PolicyOperatingSystem.linux, - ]; - const protection = 'memory_protection'; - const protectionLabel = i18n.translate( - 'xpack.securitySolution.endpoint.policy.protections.memory', - { - defaultMessage: 'Memory threat protections', - } - ); - return ( - - } - > - - - - - - - - ), - }} - /> - - - ); -}); - -MemoryProtection.displayName = 'MemoryProtection'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts deleted file mode 100644 index 6458ee0eaf4d4..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions.ts +++ /dev/null @@ -1,15 +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. - */ - -const popupVersions: Array<[string, string]> = [ - ['malware', '7.11+'], - ['ransomware', '7.12+'], - ['memory_protection', '7.15+'], - ['behavior_protection', '7.15+'], -]; - -export const popupVersionsMap: ReadonlyMap = new Map(popupVersions); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx deleted file mode 100644 index 8b4cf8d4d4877..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx +++ /dev/null @@ -1,71 +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 { FormattedMessage } from '@kbn/i18n-react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { SecurityPageName } from '../../../../../../app/types'; -import type { Immutable } from '../../../../../../../common/endpoint/types'; -import { PolicyOperatingSystem } from '../../../../../../../common/endpoint/types'; -import type { RansomwareProtectionOSes } from '../../../types'; -import { ConfigForm } from '../../components/config_form'; -import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; -import { RadioButtons } from '../components/radio_buttons'; -import { UserNotification } from '../components/user_notification'; -import { ProtectionSwitch } from '../components/protection_switch'; - -/** The Ransomware Protections form for policy details - * which will configure for all relevant OSes. - */ -export const Ransomware = React.memo(() => { - const OSes: Immutable = [PolicyOperatingSystem.windows]; - const protection = 'ransomware'; - const protectionLabel = i18n.translate( - 'xpack.securitySolution.endpoint.policy.protections.ransomware', - { - defaultMessage: 'Ransomware protections', - } - ); - - return ( - - } - > - - - - - - - - ), - }} - /> - - - ); -}); - -Ransomware.displayName = 'RansomwareProtections'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts index 26424205db010..19c3d9d6f3ea6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { @@ -13,8 +13,6 @@ import { ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, } from '@kbn/securitysolution-list-constants'; -import { useKibana } from '../../../../common/lib/kibana'; -import { useUserPrivileges } from '../../../../common/components/user_privileges'; import type { PolicyDetailsArtifactsPageLocation, PolicyDetailsState } from '../types'; import type { State } from '../../../../common/store'; import { @@ -28,7 +26,7 @@ import { getPolicyHostIsolationExceptionsPath, } from '../../../common/routing'; import { getCurrentArtifactsLocation, policyIdFromParams } from '../store/policy_details/selectors'; -import { APP_UI_ID, POLICIES_PATH } from '../../../../../common/constants'; +import { POLICIES_PATH } from '../../../../../common/constants'; /** * Narrows global state down to the PolicyDetailsState before calling the provided Policy Details Selector @@ -90,27 +88,3 @@ export const useIsPolicySettingsBarVisible = () => { window.location.pathname.includes('/settings') ); }; - -/** - * Indicates if user is granted Write access to Policy Management. This method differs from what - * `useUserPrivileges().endpointPrivileges.canWritePolicyManagement` in that it also checks if - * user has `canAccessFleet` if form is being displayed outside of Security Solution. - * This is to ensure that the Policy Form remains accessible when displayed inside of Fleet - * pages if the user does not have privileges to security solution policy management. - */ -export const useShowEditableFormFields = (): boolean => { - const { canWritePolicyManagement, canAccessFleet } = useUserPrivileges().endpointPrivileges; - const { getUrlForApp } = useKibana().services.application; - - const securitySolutionUrl = useMemo(() => { - return getUrlForApp(APP_UI_ID); - }, [getUrlForApp]); - - return useMemo(() => { - if (window.location.pathname.startsWith(securitySolutionUrl)) { - return canWritePolicyManagement; - } else { - return canAccessFleet; - } - }, [canAccessFleet, canWritePolicyManagement, securitySolutionUrl]); -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.tsx new file mode 100644 index 0000000000000..5e6a167b2ecac --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/advanced_section.tsx @@ -0,0 +1,239 @@ +/* + * 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, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiCallOut, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIconTip, + EuiPanel, + EuiSpacer, + EuiText, + EuiButtonEmpty, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { cloneDeep } from 'lodash'; +import { getEmptyValue } from '../../../../../../common/components/empty_value'; +import { useLicense } from '../../../../../../common/hooks/use_license'; +import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator'; +import type { PolicyFormComponentCommonProps } from '../types'; +import { AdvancedPolicySchema } from '../../../models/advanced_policy_schema'; + +function setValue(obj: Record, value: string, path: string[]) { + let newPolicyConfig = obj; + + // First set the value. + for (let i = 0; i < path.length - 1; i++) { + if (!newPolicyConfig[path[i]]) { + newPolicyConfig[path[i]] = {} as Record; + } + newPolicyConfig = newPolicyConfig[path[i]] as Record; + } + newPolicyConfig[path[path.length - 1]] = value; + + // Then, if the user is deleting the value, we need to ensure we clean up the config. + // We delete any sections that are empty, whether that be an empty string, empty object, or undefined. + if (value === '' || value === undefined) { + newPolicyConfig = obj; + for (let k = path.length; k >= 0; k--) { + const nextPath = path.slice(0, k); + for (let i = 0; i < nextPath.length - 1; i++) { + // Traverse and find the next section + newPolicyConfig = newPolicyConfig[nextPath[i]] as Record; + } + if ( + newPolicyConfig[nextPath[nextPath.length - 1]] === undefined || + newPolicyConfig[nextPath[nextPath.length - 1]] === '' || + Object.keys(newPolicyConfig[nextPath[nextPath.length - 1]] as object).length === 0 + ) { + // If we're looking at the `advanced` field, we leave it undefined as opposed to deleting it. + // This is because the UI looks for this field to begin rendering. + if (nextPath[nextPath.length - 1] === 'advanced') { + newPolicyConfig[nextPath[nextPath.length - 1]] = undefined; + // In all other cases, if field is empty, we'll delete it to clean up. + } else { + delete newPolicyConfig[nextPath[nextPath.length - 1]]; + } + newPolicyConfig = obj; + } else { + break; // We are looking at a non-empty section, so we can terminate. + } + } + } +} + +function getValue(obj: Record, path: string[]): string { + let currentPolicyConfig = obj; + + for (let i = 0; i < path.length - 1; i++) { + if (currentPolicyConfig[path[i]]) { + currentPolicyConfig = currentPolicyConfig[path[i]] as Record; + } else { + return ''; + } + } + return currentPolicyConfig[path[path.length - 1]] as string; +} + +const calloutTitle = i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.calloutTitle', + { + defaultMessage: 'Proceed with caution!', + } +); + +const warningMessage = i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.warningMessage', + { + defaultMessage: `This section contains policy values that support advanced use cases. If not configured + properly, these values can cause unpredictable behavior. Please consult documentation + carefully or contact support before editing these values.`, + } +); + +const HIDE = i18n.translate('xpack.securitySolution.endpoint.policy.advanced.hide', { + defaultMessage: 'Hide', +}); +const SHOW = i18n.translate('xpack.securitySolution.endpoint.policy.advanced.show', { + defaultMessage: 'Show', +}); + +export type AdvancedSectionProps = PolicyFormComponentCommonProps; + +export const AdvancedSection = memo( + ({ policy, mode, onChange, 'data-test-subj': dataTestSubj }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const [showAdvancedPolicy, setShowAdvancedPolicy] = useState(false); + const isPlatinumPlus = useLicense().isPlatinumPlus(); + + const isEditMode = mode === 'edit'; + + const handleAdvancedSettingsButtonClick = useCallback(() => { + setShowAdvancedPolicy((prevState) => !prevState); + }, []); + + const handleAdvancedSettingUpdate = useCallback( + (event) => { + const updatedPolicy = cloneDeep(policy); + + setValue( + updatedPolicy as unknown as Record, + event.target.value, + event.target.name.split('.') + ); + + onChange({ isValid: true, updatedPolicy }); + }, + [onChange, policy] + ); + + return ( +
+ + + + + + {showAdvancedPolicy && ( +
+ {isEditMode && ( + <> + +

{warningMessage}

+
+ + + )} + + +

+ +

+
+ + + {AdvancedPolicySchema.map( + ( + { + key, + documentation, + first_supported_version: firstVersion, + last_supported_version: lastVersion, + license, + }, + index + ) => { + if (!isPlatinumPlus && license === 'platinum') { + return ; + } + + const configPath = key.split('.'); + const value = getValue(policy as unknown as Record, configPath); + + return ( + + {key} + {documentation && ( + + + + )} + + } + labelAppend={ + + {lastVersion ? `${firstVersion}-${lastVersion}` : `${firstVersion}+`} + + } + > + {isEditMode ? ( + + ) : ( + {value || getEmptyValue()} + )} + + ); + } + )} + +
+ )} +
+ ); + } +); +AdvancedSection.displayName = 'AdvancedSection'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx new file mode 100644 index 0000000000000..cc1ce6915ff50 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/antivirus_registration_card.tsx @@ -0,0 +1,92 @@ +/* + * 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 } from 'react'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; +import { cloneDeep } from 'lodash'; +import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator'; +import { SettingCard } from '../setting_card'; +import type { PolicyFormComponentCommonProps } from '../../types'; + +const CARD_TITLE = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type', + { + defaultMessage: 'Register as antivirus', + } +); + +const DESCRIPTON = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', + { + defaultMessage: + 'Toggle on to register Elastic as an official Antivirus solution for Windows OS. ' + + 'This will also disable Windows Defender.', + } +); + +const REGISTERED_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type', + { + defaultMessage: 'Register as antivirus', + } +); + +const NOT_REGISTERED_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.notRegisteredLabel', + { + defaultMessage: 'Do not register as antivirus', + } +); + +type AntivirusRegistrationCardProps = PolicyFormComponentCommonProps; + +export const AntivirusRegistrationCard = memo( + ({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const isChecked = policy.windows.antivirus_registration.enabled; + const isEditMode = mode === 'edit'; + const label = isChecked ? REGISTERED_LABEL : NOT_REGISTERED_LABEL; + + const handleSwitchChange = useCallback( + (event) => { + const updatedPolicy = cloneDeep(policy); + updatedPolicy.windows.antivirus_registration.enabled = event.target.checked; + + onChange({ isValid: true, updatedPolicy }); + }, + [onChange, policy] + ); + + return ( + + {isEditMode && {DESCRIPTON}} + + + + {isEditMode ? ( + + ) : ( +
{label}
+ )} +
+ ); + } +); +AntivirusRegistrationCard.displayName = 'AntivirusRegistrationCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.tsx new file mode 100644 index 0000000000000..c2356c248261b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/attack_surface_reduction_card.tsx @@ -0,0 +1,95 @@ +/* + * 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 } from 'react'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { EuiSwitch } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { cloneDeep } from 'lodash'; +import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator'; +import { useLicense } from '../../../../../../../common/hooks/use_license'; +import { SettingLockedCard } from '../setting_locked_card'; +import type { PolicyFormComponentCommonProps } from '../../types'; +import { SettingCard } from '../setting_card'; + +const ATTACK_SURFACE_OS_LIST = [OperatingSystem.WINDOWS]; + +const LOCKED_CARD_ATTACK_SURFACE_REDUCTION = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.attack_surface_reduction', + { + defaultMessage: 'Attack Surface Reduction', + } +); + +const CARD_TITLE = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.attackSurfaceReduction.type', + { + defaultMessage: 'Attack surface reduction', + } +); + +const SWITCH_ENABLED_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.credentialHardening.toggleEnabled', + { + defaultMessage: 'Credential hardening enabled', + } +); + +const SWITCH_DISABLED_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.credentialHardening.toggleDisabled', + { + defaultMessage: 'Credential hardening disabled', + } +); + +type AttackSurfaceReductionCardProps = PolicyFormComponentCommonProps; + +export const AttackSurfaceReductionCard = memo( + ({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => { + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const getTestId = useTestIdGenerator(dataTestSubj); + const isChecked = policy.windows.attack_surface_reduction.credential_hardening.enabled; + const isEditMode = mode === 'edit'; + const label = isChecked ? SWITCH_ENABLED_LABEL : SWITCH_DISABLED_LABEL; + + const handleSwitchChange = useCallback( + (event) => { + const updatedPolicy = cloneDeep(policy); + + updatedPolicy.windows.attack_surface_reduction.credential_hardening.enabled = + event.target.checked; + + onChange({ isValid: true, updatedPolicy }); + }, + [onChange, policy] + ); + + if (!isPlatinumPlus) { + return ; + } + + return ( + + {isEditMode ? ( + + ) : ( + <>{label} + )} + + ); + } +); +AttackSurfaceReductionCard.displayName = 'AttackSurfaceReductionCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.tsx new file mode 100644 index 0000000000000..033fa855f6f5a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/behaviour_protection_card.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator'; +import { SettingCard } from '../setting_card'; +import { NotifyUserOption } from '../notify_user_option'; +import { DetectPreventProtectionLevel } from '../detect_prevent_protection_level'; +import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch'; +import type { Immutable } from '../../../../../../../../common/endpoint/types'; +import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types'; +import type { BehaviorProtectionOSes } from '../../../../types'; +import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app'; +import { APP_UI_ID, SecurityPageName } from '../../../../../../../../common'; +import { useLicense } from '../../../../../../../common/hooks/use_license'; +import { SettingLockedCard } from '../setting_locked_card'; +import type { PolicyFormComponentCommonProps } from '../../types'; + +const LOCKED_CARD_BEHAVIOR_TITLE = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.behavior', + { + defaultMessage: 'Malicious Behavior', + } +); + +const BEHAVIOUR_OS_VALUES: Immutable = [ + PolicyOperatingSystem.windows, + PolicyOperatingSystem.mac, + PolicyOperatingSystem.linux, +]; + +type BehaviourProtectionCardProps = PolicyFormComponentCommonProps; + +export const BehaviourProtectionCard = memo( + ({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => { + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const getTestId = useTestIdGenerator(dataTestSubj); + const protection = 'behavior_protection'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.behavior', + { + defaultMessage: 'Malicious behavior protections', + } + ); + + if (!isPlatinumPlus) { + return ; + } + + return ( + + } + > + + + + + + + + + + ), + }} + /> + + + ); + } +); +BehaviourProtectionCard.displayName = 'BehaviourProtectionCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/event_collection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/event_collection_card.tsx new file mode 100644 index 0000000000000..0b59cf42c2307 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/event_collection_card.tsx @@ -0,0 +1,283 @@ +/* + * 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 { ReactElement, ReactNode } from 'react'; +import React, { memo, useCallback, useContext, useMemo } from 'react'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { ThemeContext } from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { + EuiBetaBadge, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, + EuiSpacer, + EuiText, + useGeneratedHtmlId, +} from '@elastic/eui'; +import { cloneDeep, get, set } from 'lodash'; +import type { EuiCheckboxProps } from '@elastic/eui/src/components/form/checkbox/checkbox'; +import { getEmptyValue } from '../../../../../../../common/components/empty_value'; +import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator'; +import type { PolicyFormComponentCommonProps } from '../../types'; +import { SettingCard, SettingCardHeader } from '../setting_card'; +import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types'; +import type { UIPolicyConfig } from '../../../../../../../../common/endpoint/types'; + +const mapOperatingSystemToPolicyOsKey = { + [OperatingSystem.WINDOWS]: PolicyOperatingSystem.windows, + [OperatingSystem.LINUX]: PolicyOperatingSystem.linux, + [OperatingSystem.MAC]: PolicyOperatingSystem.mac, +} as const; + +type OperatingSystemToOsMap = typeof mapOperatingSystemToPolicyOsKey; + +export type ProtectionField = + keyof UIPolicyConfig[OperatingSystemToOsMap[T]]['events']; + +export type EventFormSelection = { [K in ProtectionField]: boolean }; + +export interface EventFormOption { + name: string; + protectionField: ProtectionField; +} + +export interface SupplementalEventFormOption { + id?: string; + title?: string; + description?: string; + name: string; + uncheckedName?: string; + protectionField: ProtectionField; + tooltipText?: string; + beta?: boolean; + indented?: boolean; + isDisabled?(policyConfig: UIPolicyConfig): boolean; +} + +export interface EventCollectionCardProps + extends PolicyFormComponentCommonProps { + os: T; + options: ReadonlyArray>; + selection: EventFormSelection; + supplementalOptions?: ReadonlyArray>; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ANY = any; +interface EventCollectionCardComponent { + (props: EventCollectionCardProps, context?: ANY): ReactElement< + ANY, + ANY + > | null; + displayName?: string | undefined; +} + +// eslint-disable-next-line react/display-name +export const EventCollectionCard = memo( + ({ + policy, + onChange, + mode, + os, + options, + selection, + supplementalOptions, + 'data-test-subj': dataTestSubj, + }: EventCollectionCardProps) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const isEditMode = mode === 'edit'; + const theme = useContext(ThemeContext); + const totalOptions = options.length; + const policyOs = mapOperatingSystemToPolicyOsKey[os]; + + const selectedCount: number = useMemo(() => { + const supplementalSelectionFields: string[] = supplementalOptions + ? supplementalOptions.map((value) => value.protectionField as string) + : []; + return Object.entries(selection).filter(([key, value]) => + !supplementalSelectionFields.includes(key) ? value : false + ).length; + }, [selection, supplementalOptions]); + + return ( + + {i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', + { + defaultMessage: '{selected} / {total} event collections enabled', + values: { + selected: selectedCount, + total: totalOptions, + }, + } + )} + + } + dataTestSubj={getTestId()} + > + + {i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', { + defaultMessage: 'Events', + })} + + + + {options.map(({ name, protectionField }) => { + const keyPath = `${policyOs}.events.${protectionField}`; + + return ( + + ); + })} + + {selectedCount === 0 && !isEditMode &&
{getEmptyValue()}
} + + {supplementalOptions && + supplementalOptions.map( + ({ + title, + description, + name, + uncheckedName, + protectionField, + tooltipText, + beta, + indented, + isDisabled, + }) => { + const keyPath = `${policyOs}.events.${protectionField}`; + const isChecked = get(policy, keyPath); + + if (!isEditMode && !isChecked) { + return null; + } + + return ( +
+ {title && ( + <> + + {title} + + )} + + {description && ( + <> + + + {description} + + + )} + + + + + + + + + {tooltipText && ( + + + + )} + + {beta && ( + + + + )} + +
+ ); + } + )} +
+ ); + } +) as EventCollectionCardComponent; +EventCollectionCard.displayName = 'EventCollectionCard'; + +interface EventCheckboxProps + extends PolicyFormComponentCommonProps, + Pick { + keyPath: string; + unCheckedLabel?: ReactNode; +} + +const EventCheckbox = memo( + ({ + policy, + onChange, + label, + unCheckedLabel, + mode, + keyPath, + disabled, + 'data-test-subj': dataTestSubj, + }) => { + const checkboxId = useGeneratedHtmlId(); + const isChecked: boolean = get(policy, keyPath); + const isEditMode = mode === 'edit'; + const displayLabel = isChecked ? label : unCheckedLabel ? unCheckedLabel : label; + + const checkboxOnChangeHandler = useCallback( + (ev) => { + const updatedPolicy = cloneDeep(policy); + set(updatedPolicy, keyPath, ev.target.checked); + + onChange({ isValid: true, updatedPolicy }); + }, + [keyPath, onChange, policy] + ); + + return isEditMode ? ( + + ) : isChecked ? ( +
{displayLabel}
+ ) : null; + } +); +EventCheckbox.displayName = 'EventCheckbox'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.tsx similarity index 65% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.tsx index f32f83cdf70cb..1bd1943718b29 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/linux_event_collection_card.tsx @@ -5,16 +5,13 @@ * 2.0. */ -import React, { memo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { useDispatch } from 'react-redux'; +import React, { memo, useMemo } from 'react'; import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import { setIn } from '../../../models/policy_details_config'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import type { EventFormOption, SupplementalEventFormOption } from '../../components/events_form'; -import { EventsForm } from '../../components/events_form'; -import type { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { i18n } from '@kbn/i18n'; +import type { PolicyFormComponentCommonProps } from '../../types'; +import type { UIPolicyConfig } from '../../../../../../../../common/endpoint/types'; +import type { EventFormOption, SupplementalEventFormOption } from './event_collection_card'; +import { EventCollectionCard } from './event_collection_card'; const OPTIONS: ReadonlyArray> = [ { @@ -45,6 +42,7 @@ const OPTIONS: ReadonlyArray> = [ const SUPPLEMENTAL_OPTIONS: ReadonlyArray> = [ { + id: 'sessionDataSection', title: i18n.translate( 'xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.session_data.title', { @@ -59,11 +57,17 @@ const SUPPLEMENTAL_OPTIONS: ReadonlyArray { return !config.linux.events.process; @@ -71,11 +75,17 @@ const SUPPLEMENTAL_OPTIONS: ReadonlyArray { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const dispatch = useDispatch(); +type LinuxEventCollectionCardProps = PolicyFormComponentCommonProps; + +export const LinuxEventCollectionCard = memo((props) => { + const supplementalOptions = useMemo(() => { + if (props.mode === 'edit') { + return SUPPLEMENTAL_OPTIONS; + } + + // View only mode: remove instructions for session data + return SUPPLEMENTAL_OPTIONS.map((option) => { + if (option.id === 'sessionDataSection') { + return { + ...option, + description: undefined, + }; + } + + return option; + }); + }, [props.mode]); return ( - + + {...props} os={OperatingSystem.LINUX} - selection={policyDetailsConfig.linux.events} + selection={props.policy.linux.events} + supplementalOptions={supplementalOptions} options={OPTIONS} - supplementalOptions={SUPPLEMENTAL_OPTIONS} - onValueSelection={(value, selected) => { - let newConfig = setIn(policyDetailsConfig)('linux')('events')(value)(selected); - - if (value === 'session_data' && !selected) { - newConfig = setIn(newConfig)('linux')('events')('tty_io')(false); - } - - dispatch({ - type: 'userChangedPolicyConfig', - payload: { - policyConfig: newConfig, - }, - }); - }} /> ); }); - -LinuxEvents.displayName = 'LinuxEvents'; +LinuxEventCollectionCard.displayName = 'LinuxEventCollectionCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.tsx similarity index 55% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.tsx index 7682a91daafcc..215722635c420 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/mac_event_collection_card.tsx @@ -6,14 +6,11 @@ */ import React, { memo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { useDispatch } from 'react-redux'; import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import { setIn } from '../../../models/policy_details_config'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import type { EventFormOption } from '../../components/events_form'; -import { EventsForm } from '../../components/events_form'; +import { i18n } from '@kbn/i18n'; +import type { EventFormOption } from './event_collection_card'; +import { EventCollectionCard } from './event_collection_card'; +import type { PolicyFormComponentCommonProps } from '../../types'; const OPTIONS: ReadonlyArray> = [ { @@ -36,23 +33,16 @@ const OPTIONS: ReadonlyArray> = [ }, ]; -export const MacEvents = memo(() => { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const dispatch = useDispatch(); +type MacEventCollectionCardProps = PolicyFormComponentCommonProps; +export const MacEventCollectionCard = memo((props) => { return ( - + + {...props} os={OperatingSystem.MAC} - selection={policyDetailsConfig.mac.events} + selection={props.policy.mac.events} options={OPTIONS} - onValueSelection={(value, selected) => - dispatch({ - type: 'userChangedPolicyConfig', - payload: { policyConfig: setIn(policyDetailsConfig)('mac')('events')(value)(selected) }, - }) - } /> ); }); - -MacEvents.displayName = 'MacEvents'; +MacEventCollectionCard.displayName = 'MacEventCollectionCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.tsx new file mode 100644 index 0000000000000..74dbc65737f76 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.tsx @@ -0,0 +1,205 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiCallOut, + EuiSpacer, + EuiSwitch, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, +} from '@elastic/eui'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { cloneDeep } from 'lodash'; +import { NotifyUserOption } from '../notify_user_option'; +import { SettingCard } from '../setting_card'; +import type { PolicyFormComponentCommonProps } from '../../types'; +import { APP_UI_ID } from '../../../../../../../../common'; +import { SecurityPageName } from '../../../../../../../app/types'; +import type { Immutable } from '../../../../../../../../common/endpoint/types'; +import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types'; +import type { MalwareProtectionOSes } from '../../../../types'; +import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app'; +import type { ProtectionSettingCardSwitchProps } from '../protection_setting_card_switch'; +import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch'; +import { DetectPreventProtectionLevel } from '../detect_prevent_protection_level'; +import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator'; + +const BLOCKLIST_ENABLED_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.blocklistEnabled', + { + defaultMessage: 'Blocklist enabled', + } +); + +const BLOCKLIST_DISABLED_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.blocklistDisabled', + { + defaultMessage: 'Blocklist disabled', + } +); + +// NOTE: it mutates `policyConfigData` passed on input +const adjustBlocklistSettingsOnProtectionSwitch: ProtectionSettingCardSwitchProps['additionalOnSwitchChange'] = + ({ value, policyConfigData, protectionOsList }) => { + for (const os of protectionOsList) { + policyConfigData[os].malware.blocklist = value; + } + + return policyConfigData; + }; + +const MALWARE_OS_VALUES: Immutable = [ + PolicyOperatingSystem.windows, + PolicyOperatingSystem.mac, + PolicyOperatingSystem.linux, +]; + +export type MalwareProtectionsProps = PolicyFormComponentCommonProps; + +/** The Malware Protections form for policy details + * which will configure for all relevant OSes. + */ +export const MalwareProtectionsCard = React.memo( + ({ policy, onChange, mode = 'edit', 'data-test-subj': dataTestSubj }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + const protection = 'malware'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.malware', + { + defaultMessage: 'Malware protections', + } + ); + + return ( + + } + > + + + + + + + + + + + + + ), + }} + /> + + + ); + } +); + +MalwareProtectionsCard.displayName = 'MalwareProtectionsCard'; + +type EnableDisableBlocklistProps = PolicyFormComponentCommonProps; + +const EnableDisableBlocklist = memo(({ policy, onChange, mode }) => { + const checked = policy.windows.malware.blocklist; + const isDisabled = policy.windows.malware.mode === 'off'; + const isEditMode = mode === 'edit'; + const label = checked ? BLOCKLIST_ENABLED_LABEL : BLOCKLIST_DISABLED_LABEL; + + const handleBlocklistSwitchChange = useCallback( + (event) => { + const value = event.target.checked; + const newPayload = cloneDeep(policy); + + adjustBlocklistSettingsOnProtectionSwitch({ + value, + policyConfigData: newPayload, + protectionOsList: MALWARE_OS_VALUES, + }); + + onChange({ isValid: true, updatedPolicy: newPayload }); + }, + [onChange, policy] + ); + + return ( + + + {isEditMode ? ( + + ) : ( + <>{label} + )} + + + + + + } + /> + + + ); +}); +EnableDisableBlocklist.displayName = 'EnableDisableBlocklist'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.tsx new file mode 100644 index 0000000000000..3af9a422fc1a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/memory_protection_card.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator'; +import { NotifyUserOption } from '../notify_user_option'; +import { DetectPreventProtectionLevel } from '../detect_prevent_protection_level'; +import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch'; +import { SettingLockedCard } from '../setting_locked_card'; +import type { Immutable } from '../../../../../../../../common/endpoint/types'; +import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types'; +import type { MemoryProtectionOSes } from '../../../../types'; +import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app'; +import { APP_UI_ID, SecurityPageName } from '../../../../../../../../common'; +import { useLicense } from '../../../../../../../common/hooks/use_license'; +import type { PolicyFormComponentCommonProps } from '../../types'; +import { SettingCard } from '../setting_card'; + +const LOCKED_CARD_MEMORY_TITLE = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.memory', + { + defaultMessage: 'Memory Threat', + } +); + +const MEMORY_PROTECTION_OS_VALUES: Immutable = [ + PolicyOperatingSystem.windows, + PolicyOperatingSystem.mac, + PolicyOperatingSystem.linux, +]; + +type MemoryProtectionCardProps = PolicyFormComponentCommonProps; + +export const MemoryProtectionCard = memo( + ({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => { + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const getTestId = useTestIdGenerator(dataTestSubj); + const protection = 'memory_protection'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.memory', + { + defaultMessage: 'Memory threat protections', + } + ); + + if (!isPlatinumPlus) { + return ; + } + + return ( + + } + > + + + + + + + + + + ), + }} + /> + + + ); + } +); +MemoryProtectionCard.displayName = 'MemoryProtectionCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.tsx new file mode 100644 index 0000000000000..b1988ab53c482 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/ransomware_protection_card.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useTestIdGenerator } from '../../../../../../hooks/use_test_id_generator'; +import { ProtectionSettingCardSwitch } from '../protection_setting_card_switch'; +import { NotifyUserOption } from '../notify_user_option'; +import { DetectPreventProtectionLevel } from '../detect_prevent_protection_level'; +import { SettingCard } from '../setting_card'; +import type { PolicyFormComponentCommonProps } from '../../types'; +import type { Immutable } from '../../../../../../../../common/endpoint/types'; +import { PolicyOperatingSystem } from '../../../../../../../../common/endpoint/types'; +import type { RansomwareProtectionOSes } from '../../../../types'; +import { LinkToApp } from '../../../../../../../common/components/endpoint/link_to_app'; +import { APP_UI_ID, SecurityPageName } from '../../../../../../../../common'; +import { useLicense } from '../../../../../../../common/hooks/use_license'; +import { SettingLockedCard } from '../setting_locked_card'; + +const RANSOMEWARE_OS_VALUES: Immutable = [ + PolicyOperatingSystem.windows, +]; + +const LOCKED_CARD_RAMSOMWARE_TITLE = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.ransomware', + { + defaultMessage: 'Ransomware', + } +); + +type RansomwareProtectionCardProps = PolicyFormComponentCommonProps; + +export const RansomwareProtectionCard = React.memo( + ({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => { + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const getTestId = useTestIdGenerator(dataTestSubj); + const protection = 'ransomware'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.ransomware', + { + defaultMessage: 'Ransomware protections', + } + ); + + if (!isPlatinumPlus) { + return ; + } + + return ( + + } + > + + + + + + + + + + ), + }} + /> + + + ); + } +); +RansomwareProtectionCard.displayName = 'RansomwareProtectionCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.tsx similarity index 71% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.tsx index 9ba51a819e041..33653e2f603a7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/windows_event_collection_card.tsx @@ -7,13 +7,10 @@ import React, { memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { useDispatch } from 'react-redux'; import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { policyConfig } from '../../../store/policy_details/selectors'; -import { setIn } from '../../../models/policy_details_config'; -import { usePolicyDetailsSelector } from '../../policy_hooks'; -import type { EventFormOption } from '../../components/events_form'; -import { EventsForm } from '../../components/events_form'; +import type { EventFormOption } from './event_collection_card'; +import { EventCollectionCard } from './event_collection_card'; +import type { PolicyFormComponentCommonProps } from '../../types'; const OPTIONS: ReadonlyArray> = [ { @@ -87,25 +84,16 @@ const OPTIONS: ReadonlyArray> = [ }, ]; -export const WindowsEvents = memo(() => { - const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); - const dispatch = useDispatch(); +type WindowsEventCollectionCardProps = PolicyFormComponentCommonProps; +export const WindowsEventCollectionCard = memo((props) => { return ( - + + {...props} os={OperatingSystem.WINDOWS} - selection={policyDetailsConfig.windows.events} + selection={props.policy.windows.events} options={OPTIONS} - onValueSelection={(value, selected) => - dispatch({ - type: 'userChangedPolicyConfig', - payload: { - policyConfig: setIn(policyDetailsConfig)('windows')('events')(value)(selected), - }, - }) - } /> ); }); - -WindowsEvents.displayName = 'WindowsEvents'; +WindowsEventCollectionCard.displayName = 'WindowsEventCollectionCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.tsx new file mode 100644 index 0000000000000..a141234ded60e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/detect_prevent_protection_level.tsx @@ -0,0 +1,185 @@ +/* + * 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, useGeneratedHtmlId } 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, + UIPolicyConfig, + Immutable, +} from '../../../../../../../common/endpoint/types'; +import { ProtectionModes } from '../../../../../../../common/endpoint/types'; +import type { MacPolicyProtection, LinuxPolicyProtection, PolicyProtection } from '../../../types'; +import { useLicense } from '../../../../../../common/hooks/use_license'; + +const DETECT_LABEL = i18n.translate('xpack.securitySolution.endpoint.policy.details.detect', { + defaultMessage: 'Detect', +}); + +const PREVENT_LABEL = i18n.translate('xpack.securitySolution.endpoint.policy.details.prevent', { + defaultMessage: 'Prevent', +}); + +export type DetectPreventProtectionLavelProps = PolicyFormComponentCommonProps & { + protection: PolicyProtection; + osList: ImmutableArray>; +}; + +export const DetectPreventProtectionLevel = memo( + ({ policy, protection, osList, mode, onChange, 'data-test-subj': dataTestSubj }) => { + const isEditMode = mode === 'edit'; + const getTestId = useTestIdGenerator(dataTestSubj); + + const radios: Immutable< + Array<{ + id: ProtectionModes; + label: string; + flexGrow: EuiFlexItemProps['grow']; + }> + > = useMemo(() => { + return [ + { + id: ProtectionModes.detect, + label: DETECT_LABEL, + flexGrow: 1, + }, + { + id: ProtectionModes.prevent, + label: PREVENT_LABEL, + flexGrow: 5, + }, + ]; + }, []); + + const currentProtectionLevelLabel = useMemo(() => { + const radio = radios.find((item) => item.id === policy.windows[protection].mode); + + if (radio) { + return radio.label; + } + + return PREVENT_LABEL; + }, [policy.windows, protection, radios]); + + return ( +
+ + + + + + {isEditMode ? ( + radios.map(({ label, id, flexGrow }) => { + return ( + + + + ); + }) + ) : ( + <>{currentProtectionLevelLabel} + )} + +
+ ); + } +); +DetectPreventProtectionLevel.displayName = 'DetectPreventProtectionLevel'; + +interface ProtectionRadioProps extends PolicyFormComponentCommonProps { + protection: PolicyProtection; + protectionMode: ProtectionModes; + osList: ImmutableArray>; + label: string; +} + +const ProtectionRadio = React.memo( + ({ + protection, + protectionMode, + osList, + label, + onChange, + policy, + mode, + 'data-test-subj': dataTestSubj, + }: ProtectionRadioProps) => { + const radioButtonId = useGeneratedHtmlId(); + const selected = policy.windows[protection].mode; + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const showEditableFormFields = mode === 'edit'; + + const handleRadioChange = useCallback(() => { + const newPayload = cloneDeep(policy); + + for (const os of osList) { + if (os === 'windows') { + newPayload[os][protection].mode = protectionMode; + } else if (os === 'mac') { + newPayload[os][protection as MacPolicyProtection].mode = protectionMode; + } else if (os === 'linux') { + newPayload[os][protection as LinuxPolicyProtection].mode = protectionMode; + } + if (isPlatinumPlus) { + if (os === 'windows') { + if (protectionMode === ProtectionModes.prevent) { + newPayload[os].popup[protection].enabled = true; + } else { + newPayload[os].popup[protection].enabled = false; + } + } else if (os === 'mac') { + if (protectionMode === ProtectionModes.prevent) { + newPayload[os].popup[protection as MacPolicyProtection].enabled = true; + } else { + newPayload[os].popup[protection as MacPolicyProtection].enabled = false; + } + } else if (os === 'linux') { + if (protectionMode === ProtectionModes.prevent) { + newPayload[os].popup[protection as LinuxPolicyProtection].enabled = true; + } else { + newPayload[os].popup[protection as LinuxPolicyProtection].enabled = false; + } + } + } + } + + onChange({ isValid: true, updatedPolicy: newPayload }); + }, [isPlatinumPlus, onChange, osList, policy, protection, protectionMode]); + + return ( + + ); + } +); + +ProtectionRadio.displayName = 'ProtectionRadio'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx new file mode 100644 index 0000000000000..f7afe7bd186d6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/notify_user_option.tsx @@ -0,0 +1,291 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { cloneDeep } from 'lodash'; +import { + EuiSpacer, + EuiFlexItem, + EuiFlexGroup, + EuiCheckbox, + EuiIconTip, + EuiText, + EuiTextArea, +} from '@elastic/eui'; +import { PROTECTION_NOTICE_SUPPORTED_ENDPOINT_VERSION } from '../protection_notice_supported_endpoint_version'; +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 type { ImmutableArray, UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { ProtectionModes } from '../../../../../../../common/endpoint/types'; +import type { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types'; + +const NOTIFY_USER_CHECKBOX_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetail.notifyUser', + { + defaultMessage: 'Notify user', + } +); + +const DO_NOT_NOTIFY_USER_CHECKBOX_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetail.doNotNotifyUser', + { + defaultMessage: "Don't notify user", + } +); + +const NOTIFICATION_MESSAGE_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.notificationMessage', + { + defaultMessage: 'Notification message', + } +); + +const CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.customizeUserNotification', + { + defaultMessage: 'Customize notification message', + } +); + +interface NotifyUserOptionProps extends PolicyFormComponentCommonProps { + protection: PolicyProtection; + osList: ImmutableArray>; +} + +export const NotifyUserOption = React.memo( + ({ + policy, + onChange, + mode, + protection, + osList, + 'data-test-subj': dataTestSubj, + }: NotifyUserOptionProps) => { + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const getTestId = useTestIdGenerator(dataTestSubj); + + const isEditMode = mode === 'edit'; + const selected = policy.windows[protection].mode; + const userNotificationSelected = policy.windows.popup[protection].enabled; + const userNotificationMessage = policy.windows.popup[protection].message; + const checkboxLabel = userNotificationSelected + ? NOTIFY_USER_CHECKBOX_LABEL + : DO_NOT_NOTIFY_USER_CHECKBOX_LABEL; + + const handleUserNotificationCheckbox = useCallback( + (event) => { + const newPayload = cloneDeep(policy); + + for (const os of osList) { + if (os === 'windows') { + newPayload[os].popup[protection].enabled = event.target.checked; + } else if (os === 'mac') { + newPayload[os].popup[protection as MacPolicyProtection].enabled = event.target.checked; + } else if (os === 'linux') { + newPayload[os].popup[protection as LinuxPolicyProtection].enabled = + event.target.checked; + } + } + + onChange({ isValid: true, updatedPolicy: newPayload }); + }, + [policy, onChange, osList, protection] + ); + + const handleCustomUserNotification = useCallback( + (event) => { + const newPayload = cloneDeep(policy); + for (const os of osList) { + if (os === 'windows') { + newPayload[os].popup[protection].message = event.target.value; + } else if (os === 'mac') { + newPayload[os].popup[protection as MacPolicyProtection].message = event.target.value; + } else if (os === 'linux') { + newPayload[os].popup[protection as LinuxPolicyProtection].message = event.target.value; + } + } + + onChange({ isValid: true, updatedPolicy: newPayload }); + }, + [policy, onChange, osList, protection] + ); + + const tooltipProtectionText = useCallback((protectionType: PolicyProtection) => { + if (protectionType === 'memory_protection') { + return i18n.translate( + 'xpack.securitySolution.endpoint.policyDetail.memoryProtectionTooltip', + { + defaultMessage: 'memory threat', + } + ); + } else if (protectionType === 'behavior_protection') { + return i18n.translate( + 'xpack.securitySolution.endpoint.policyDetail.behaviorProtectionTooltip', + { + defaultMessage: 'malicious behavior', + } + ); + } else { + return protectionType; + } + }, []); + + const tooltipBracketText = useCallback( + (protectionType: PolicyProtection) => { + if (protectionType === 'memory_protection' || protection === 'behavior_protection') { + return i18n.translate('xpack.securitySolution.endpoint.policyDetail.rule', { + defaultMessage: 'rule', + }); + } else { + return i18n.translate('xpack.securitySolution.endpoint.policyDetail.filename', { + defaultMessage: 'filename', + }); + } + }, + [protection] + ); + + if (!isPlatinumPlus) { + return null; + } + + return ( +
+ + + + + + + + + + {isEditMode ? ( + + ) : ( + <>{checkboxLabel} + )} + + {userNotificationSelected && + (isEditMode ? ( + <> + + + + +

{CUSTOMIZE_NOTIFICATION_MESSAGE_LABEL}

+
+
+ + + + + + + } + /> + +
+ + + + ) : ( + <> + + +

{NOTIFICATION_MESSAGE_LABEL}

+
+ + <>{userNotificationMessage || getEmptyValue()} + + ))} +
+ ); + } +); +NotifyUserOption.displayName = 'NotifyUserOption'; + +export const SupportedVersionForProtectionNotice = React.memo( + ({ + protection, + 'data-test-subj': dataTestSubj, + }: { + protection: string; + 'data-test-subj'?: string; + }) => { + const version = useMemo(() => { + return PROTECTION_NOTICE_SUPPORTED_ENDPOINT_VERSION[ + protection as keyof typeof PROTECTION_NOTICE_SUPPORTED_ENDPOINT_VERSION + ]; + }, [protection]); + + if (!version) { + return null; + } + + return ( + + + + + + ); + } +); +SupportedVersionForProtectionNotice.displayName = 'SupportedVersionForProtectionNotice'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.tsx new file mode 100644 index 0000000000000..cdd636b9c4baa --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/protection_setting_card_switch.tsx @@ -0,0 +1,140 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiSwitch } from '@elastic/eui'; +import { cloneDeep } from 'lodash'; +import type { PolicyFormComponentCommonProps } from '../types'; +import { useLicense } from '../../../../../../common/hooks/use_license'; +import type { + ImmutableArray, + UIPolicyConfig, + PolicyConfig, +} from '../../../../../../../common/endpoint/types'; +import { ProtectionModes } from '../../../../../../../common/endpoint/types'; +import type { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '../../../types'; + +export interface ProtectionSettingCardSwitchProps extends PolicyFormComponentCommonProps { + protection: PolicyProtection; + protectionLabel?: string; + osList: ImmutableArray>; + additionalOnSwitchChange?: ({ + value, + policyConfigData, + protectionOsList, + }: { + value: boolean; + policyConfigData: PolicyConfig; + protectionOsList: ImmutableArray>; + }) => PolicyConfig; +} + +export const ProtectionSettingCardSwitch = React.memo( + ({ + protection, + protectionLabel, + osList, + additionalOnSwitchChange, + onChange, + policy, + mode, + 'data-test-subj': dataTestSubj, + }: ProtectionSettingCardSwitchProps) => { + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const isEditMode = mode === 'edit'; + const selected = policy && policy.windows[protection].mode; + const switchLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.protectionsEnabled', + { + defaultMessage: '{protectionLabel} {mode, select, true {enabled} false {disabled}}', + values: { + protectionLabel, + mode: selected !== ProtectionModes.off, + }, + } + ); + + const handleSwitchChange = useCallback( + (event) => { + const newPayload = cloneDeep(policy); + + if (event.target.checked === false) { + for (const os of osList) { + if (os === 'windows') { + newPayload[os][protection].mode = ProtectionModes.off; + } else if (os === 'mac') { + newPayload[os][protection as MacPolicyProtection].mode = ProtectionModes.off; + } else if (os === 'linux') { + newPayload[os][protection as LinuxPolicyProtection].mode = ProtectionModes.off; + } + if (isPlatinumPlus) { + if (os === 'windows') { + newPayload[os].popup[protection].enabled = event.target.checked; + } else if (os === 'mac') { + newPayload[os].popup[protection as MacPolicyProtection].enabled = + event.target.checked; + } else if (os === 'linux') { + newPayload[os].popup[protection as LinuxPolicyProtection].enabled = + event.target.checked; + } + } + } + } else { + for (const os of osList) { + if (os === 'windows') { + newPayload[os][protection].mode = ProtectionModes.prevent; + } else if (os === 'mac') { + newPayload[os][protection as MacPolicyProtection].mode = ProtectionModes.prevent; + } else if (os === 'linux') { + newPayload[os][protection as LinuxPolicyProtection].mode = ProtectionModes.prevent; + } + if (isPlatinumPlus) { + if (os === 'windows') { + newPayload[os].popup[protection].enabled = event.target.checked; + } else if (os === 'mac') { + newPayload[os].popup[protection as MacPolicyProtection].enabled = + event.target.checked; + } else if (os === 'linux') { + newPayload[os].popup[protection as LinuxPolicyProtection].enabled = + event.target.checked; + } + } + } + } + + onChange({ + isValid: true, + updatedPolicy: additionalOnSwitchChange + ? additionalOnSwitchChange({ + value: event.target.checked, + policyConfigData: newPayload, + protectionOsList: osList, + }) + : newPayload, + }); + }, + [policy, onChange, additionalOnSwitchChange, osList, isPlatinumPlus, protection] + ); + + if (!isEditMode) { + return <>{switchLabel}; + } + + return ( + + ); + } +); + +ProtectionSettingCardSwitch.displayName = 'ProtectionSettingCardSwitch'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_card.tsx similarity index 77% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_card.tsx index b5f46c91dade4..ebae19d960b69 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_card.tsx @@ -23,6 +23,7 @@ import { import { ThemeContext } from 'styled-components'; import type { OperatingSystem } from '@kbn/securitysolution-utils'; +import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator'; import { OS_TITLES } from '../../../../../common/translations'; const TITLES = { @@ -34,7 +35,7 @@ const TITLES = { }), }; -interface ConfigFormProps { +interface SettingCardProps { /** * A subtitle for this component. **/ @@ -49,19 +50,22 @@ interface ConfigFormProps { rightCorner?: ReactNode; } -export const ConfigFormHeading: FC = memo(({ children }) => ( - -
{children}
-
-)); - -ConfigFormHeading.displayName = 'ConfigFormHeading'; +export const SettingCardHeader = memo<{ children: React.ReactNode; 'data-test-subj'?: string }>( + ({ children, 'data-test-subj': dataTestSubj }) => ( + +
{children}
+
+ ) +); +SettingCardHeader.displayName = 'SettingCardHeader'; -export const ConfigForm: FC = memo( +export const SettingCard: FC = memo( ({ type, supportedOss, osRestriction, dataTestSubj, rightCorner, children }) => { const paddingSize = useContext(ThemeContext).eui.euiPanelPaddingModifiers.paddingMedium; + const getTestId = useTestIdGenerator(dataTestSubj); + return ( - + = memo( style={{ padding: `${paddingSize} ${paddingSize} 0 ${paddingSize}` }} > - {TITLES.type} + {TITLES.type} {type} - {TITLES.os} + {TITLES.os} - {supportedOss.map((os) => OS_TITLES[os]).join(', ')} + + {supportedOss.map((os) => OS_TITLES[os]).join(', ')}{' '} + {osRestriction && ( @@ -117,4 +123,4 @@ export const ConfigForm: FC = memo( } ); -ConfigForm.displayName = 'ConfigForm'; +SettingCard.displayName = 'SettingCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx new file mode 100644 index 0000000000000..bb2faa6f8abe2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/setting_locked_card.tsx @@ -0,0 +1,98 @@ +/* + * 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, + EuiTextColor, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator'; + +const LockedPolicyDiv = styled.div` + .euiCard__betaBadgeWrapper { + .euiCard__betaBadge { + width: auto; + } + } + .lockedCardDescription { + padding: 0 33.3%; + } +`; + +export const SettingLockedCard = memo( + ({ title, 'data-test-subj': dataTestSubj }: { title: string; 'data-test-subj'?: string }) => { + const getTestId = useTestIdGenerator(dataTestSubj); + + return ( + + } + title={ +

+ {title} +

+ } + description={false} + > + + + +

+ + + +

+
+ +

+ + + + ), + }} + /> +

+
+
+
+
+
+ ); + } +); +SettingLockedCard.displayName = 'SettingLockedCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/index.ts similarity index 80% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/index.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/index.ts index cf989e6b4e0ee..b84c32fbeee97 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { PolicyFormLayout } from './policy_form_layout'; +export { PolicySettingsForm } from './policy_settings_form'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts new file mode 100644 index 0000000000000..c351bea4fdf93 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/mocks.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +interface TestSubjGenerator { + (suffix?: string): string; + withPrefix: (prefix: string) => TestSubjGenerator; +} + +export const createTestSubjGenerator = (testSubjPrefix: string): TestSubjGenerator => { + const testSubjGenerator: TestSubjGenerator = (suffix) => { + if (suffix) { + return `${testSubjPrefix}-${suffix}`; + } + return testSubjPrefix; + }; + + testSubjGenerator.withPrefix = (prefix: string): TestSubjGenerator => { + return createTestSubjGenerator(testSubjGenerator(prefix)); + }; + + return testSubjGenerator; +}; + +export const getPolicySettingsFormTestSubjects = ( + formTopLevelTestSubj: string = 'endpointPolicyForm' +) => { + const genTestSubj = createTestSubjGenerator(formTopLevelTestSubj); + const malwareTestSubj = genTestSubj.withPrefix('malware'); + const ransomwareTestSubj = genTestSubj.withPrefix('ransomware'); + const memoryTestSubj = genTestSubj.withPrefix('memory'); + const behaviourTestSubj = genTestSubj.withPrefix('behaviour'); + const advancedSectionTestSubj = genTestSubj.withPrefix('advancedSection'); + const windowsEventsTestSubj = genTestSubj.withPrefix('windowsEvents'); + const macEventsTestSubj = genTestSubj.withPrefix('macEvents'); + const linuxEventsTestSubj = genTestSubj.withPrefix('linuxEvents'); + + const testSubj = { + form: genTestSubj(), + + malware: { + card: malwareTestSubj(), + enableDisableSwitch: malwareTestSubj('enableDisableSwitch'), + protectionPreventRadio: malwareTestSubj('protectionLevel-preventRadio'), + protectionDetectRadio: malwareTestSubj('protectionLevel-detectRadio'), + notifyUserCheckbox: malwareTestSubj('notifyUser-checkbox'), + notifySupportedVersion: malwareTestSubj('notifyUser-supportedVersion'), + notifyCustomMessage: malwareTestSubj('notifyUser-customMessage'), + notifyCustomMessageTooltipIcon: malwareTestSubj('notifyUser-tooltipIcon'), + notifyCustomMessageTooltipInfo: malwareTestSubj('notifyUser-tooltipInfo'), + osValuesContainer: malwareTestSubj('osValues'), + }, + ransomware: { + card: ransomwareTestSubj(), + enableDisableSwitch: ransomwareTestSubj('enableDisableSwitch'), + protectionPreventRadio: ransomwareTestSubj('protectionLevel-preventRadio'), + protectionDetectRadio: ransomwareTestSubj('protectionLevel-detectRadio'), + notifyUserCheckbox: ransomwareTestSubj('notifyUser-checkbox'), + notifySupportedVersion: ransomwareTestSubj('notifyUser-supportedVersion'), + notifyCustomMessage: ransomwareTestSubj('notifyUser-customMessage'), + notifyCustomMessageTooltipIcon: ransomwareTestSubj('notifyUser-tooltipIcon'), + notifyCustomMessageTooltipInfo: ransomwareTestSubj('notifyUser-tooltipInfo'), + osValuesContainer: ransomwareTestSubj('osValues'), + }, + memory: { + card: memoryTestSubj(), + enableDisableSwitch: memoryTestSubj('enableDisableSwitch'), + protectionPreventRadio: memoryTestSubj('protectionLevel-preventRadio'), + protectionDetectRadio: memoryTestSubj('protectionLevel-detectRadio'), + notifyUserCheckbox: memoryTestSubj('notifyUser-checkbox'), + osValuesContainer: memoryTestSubj('osValues'), + }, + behaviour: { + card: behaviourTestSubj(), + enableDisableSwitch: behaviourTestSubj('enableDisableSwitch'), + protectionPreventRadio: behaviourTestSubj('protectionLevel-preventRadio'), + protectionDetectRadio: behaviourTestSubj('protectionLevel-detectRadio'), + notifyUserCheckbox: behaviourTestSubj('notifyUser-checkbox'), + osValuesContainer: behaviourTestSubj('osValues'), + }, + attachSurface: { + card: genTestSubj('attackSurface'), + enableDisableSwitch: genTestSubj('attachSurface-enableDisableSwitch'), + osValuesContainer: genTestSubj('attackSurface-osValues'), + }, + + windowsEvents: { + card: windowsEventsTestSubj(), + dnsCheckbox: windowsEventsTestSubj('dns'), + processCheckbox: windowsEventsTestSubj('process'), + fileCheckbox: windowsEventsTestSubj('file'), + }, + macEvents: { + card: macEventsTestSubj(), + fileCheckbox: macEventsTestSubj('file'), + }, + linuxEvents: { + card: linuxEventsTestSubj(), + fileCheckbox: linuxEventsTestSubj('file'), + }, + antivirusRegistration: { + card: genTestSubj('antivirusRegistration'), + }, + advancedSection: { + container: advancedSectionTestSubj(''), + showHideButton: advancedSectionTestSubj('showButton'), + settingsContainer: advancedSectionTestSubj('settings'), + warningCallout: advancedSectionTestSubj('warning'), + }, + }; + + return testSubj; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx new file mode 100644 index 0000000000000..8771e40e4be7c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx @@ -0,0 +1,87 @@ +/* + * 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 { EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AntivirusRegistrationCard } from './components/cards/antivirus_registration_card'; +import { LinuxEventCollectionCard } from './components/cards/linux_event_collection_card'; +import { MacEventCollectionCard } from './components/cards/mac_event_collection_card'; +import { WindowsEventCollectionCard } from './components/cards/windows_event_collection_card'; +import { AttackSurfaceReductionCard } from './components/cards/attack_surface_reduction_card'; +import { BehaviourProtectionCard } from './components/cards/behaviour_protection_card'; +import { MemoryProtectionCard } from './components/cards/memory_protection_card'; +import { RansomwareProtectionCard } from './components/cards/ransomware_protection_card'; +import { MalwareProtectionsCard } from './components/cards/malware_protections_card'; +import type { PolicyFormComponentCommonProps } from './types'; +import { AdvancedSection } from './components/advanced_section'; +import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator'; + +const PROTECTIONS_SECTION_TITLE = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.protections', + { defaultMessage: 'Protections' } +); + +const SETTINGS_SECTION_TITLE = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.settings', + { defaultMessage: 'Settings' } +); + +export type PolicySettingsFormProps = PolicyFormComponentCommonProps; + +export const PolicySettingsForm = memo((props) => { + const getTestId = useTestIdGenerator(props['data-test-subj']); + + return ( +
+ {PROTECTIONS_SECTION_TITLE} + + + + + + + + + + + + + + + + + + {SETTINGS_SECTION_TITLE} + + + + + + + + + + + + + + + +
+ ); +}); +PolicySettingsForm.displayName = 'PolicySettingsForm'; + +const FormSectionTitle = memo(({ children }) => { + return ( + +

{children}

+
+ ); +}); +FormSectionTitle.displayName = 'FormSectionTitle'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/protection_notice_supported_endpoint_version.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/protection_notice_supported_endpoint_version.ts new file mode 100644 index 0000000000000..5e518cb6215a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/protection_notice_supported_endpoint_version.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const PROTECTION_NOTICE_SUPPORTED_ENDPOINT_VERSION = Object.freeze({ + malware: '7.11+', + ransomware: '7.12+', + memory_protection: '7.15+', + behavior_protection: '7.15+', +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/types.ts new file mode 100644 index 0000000000000..d09f8005750ee --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/types.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PolicyConfig } from '../../../../../../common/endpoint/types'; + +export interface PolicyFormComponentCommonProps { + policy: PolicyConfig; + onChange: (options: { isValid: boolean; updatedPolicy: PolicyConfig }) => void; + mode: 'edit' | 'view'; + 'data-test-subj'?: string; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_confirm_update.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/components/policy_form_confirm_update.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_confirm_update.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/components/policy_form_confirm_update.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/index.ts similarity index 68% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/index.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/index.ts index 53f02b8e5349e..34ce6db6a0bb2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/index.ts @@ -5,6 +5,4 @@ * 2.0. */ -export { WindowsEvents } from './windows'; -export { MacEvents } from './mac'; -export { LinuxEvents } from './linux'; +export { PolicySettingsLayout } from './policy_settings_layout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.tsx similarity index 51% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.tsx index 34defb5b25ce6..bd7ecc6529c65 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.tsx @@ -5,64 +5,58 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiButtonEmpty, - EuiLoadingSpinner, - EuiSpacer, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { useDispatch } from 'react-redux'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; -import type { ApplicationStart } from '@kbn/core/public'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import type { ApplicationStart } from '@kbn/core-application-browser'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; -import { useShowEditableFormFields, usePolicyDetailsSelector } from '../../policy_hooks'; -import { - policyDetails, - agentStatusSummary, - updateStatus, - isLoading, -} from '../../../store/policy_details/selectors'; - -import { useToasts, useKibana } from '../../../../../../common/lib/kibana'; -import type { AppAction } from '../../../../../../common/store/actions'; -import { getPoliciesPath } from '../../../../../common/routing'; -import { useNavigateToAppEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import type { PolicyDetailsRouteState } from '../../../../../../../common/endpoint/types'; -import { SecuritySolutionPageWrapper } from '../../../../../../common/components/page_wrapper'; -import { PolicyDetailsForm } from '../../policy_details_form'; -import { ConfirmUpdate } from './policy_form_confirm_update'; - -export const PolicyFormLayout = React.memo(() => { - const dispatch = useDispatch<(action: AppAction) => void>(); +import { cloneDeep } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; +import { useFetchAgentByAgentPolicySummary } from '../../../../hooks/policy/use_fetch_endpoint_policy_agent_summary'; +import { useUpdateEndpointPolicy } from '../../../../hooks/policy/use_update_endpoint_policy'; +import type { PolicySettingsFormProps } from '../policy_settings_form/policy_settings_form'; +import { PolicySettingsForm } from '../policy_settings_form'; +import type { + MaybeImmutable, + PolicyConfig, + PolicyData, + PolicyDetailsRouteState, +} from '../../../../../../common/endpoint/types'; +import { useKibana, useToasts } from '../../../../../common/lib/kibana'; +import { APP_UI_ID } from '../../../../../../common'; +import { getPoliciesPath } from '../../../../common/routing'; +import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { ConfirmUpdate } from './components/policy_form_confirm_update'; + +export interface PolicySettingsLayoutProps { + policy: MaybeImmutable; +} + +export const PolicySettingsLayout = memo(({ policy: _policy }) => { + const policy = _policy as PolicyData; const { services: { - theme, application: { navigateToApp }, }, } = useKibana(); const toasts = useToasts(); const { state: locationRouteState } = useLocation(); - const showEditableFormFields = useShowEditableFormFields(); - - // Store values - const policyItem = usePolicyDetailsSelector(policyDetails); - const policyAgentStatusSummary = usePolicyDetailsSelector(agentStatusSummary); - const policyUpdateStatus = usePolicyDetailsSelector(updateStatus); - const isPolicyLoading = usePolicyDetailsSelector(isLoading); + const { canWritePolicyManagement } = useUserPrivileges().endpointPrivileges; + const { isLoading: isUpdating, mutateAsync: sendPolicyUpdate } = useUpdateEndpointPolicy(); + const { data: agentSummaryData } = useFetchAgentByAgentPolicySummary(policy.policy_id); - // Local state + const [policySettings, setPolicySettings] = useState( + cloneDeep(policy.inputs[0].config.policy.value) + ); const [showConfirm, setShowConfirm] = useState(false); const [routeState, setRouteState] = useState(); - const policyName = policyItem?.name ?? ''; + const isEditMode = canWritePolicyManagement; + const policyName = policy?.name ?? ''; const routingOnCancelNavigateTo = routeState?.onCancelNavigateTo; + const navigateToAppArguments = useMemo((): Parameters => { if (routingOnCancelNavigateTo) { return routingOnCancelNavigateTo; @@ -76,60 +70,68 @@ export const PolicyFormLayout = React.memo(() => { ]; }, [routingOnCancelNavigateTo]); - // Handle showing update statuses - useEffect(() => { - if (policyUpdateStatus) { - if (policyUpdateStatus.success) { + const handleSettingsOnChange: PolicySettingsFormProps['onChange'] = useCallback((updates) => { + setPolicySettings(updates.updatedPolicy); + }, []); + + const handleCancelOnClick = useNavigateToAppEventHandler(...navigateToAppArguments); + + const handleSaveOnClick = useCallback(() => { + setShowConfirm(true); + }, []); + + const handleSaveCancel = useCallback(() => { + setShowConfirm(false); + }, []); + + const handleSaveConfirmation = useCallback(() => { + const update = cloneDeep(policy); + + update.inputs[0].config.policy.value = policySettings; + sendPolicyUpdate({ policy: update }) + .then(() => { toasts.addSuccess({ + 'data-test-subj': 'policyDetailsSuccessMessage', title: i18n.translate( 'xpack.securitySolution.endpoint.policy.details.updateSuccessTitle', { defaultMessage: 'Success!', } ), - text: toMountPoint( - - - , - { theme$: theme.theme$ } + text: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.updateSuccessMessage', + { + defaultMessage: 'Integration {name} has been updated.', + values: { name: policyName }, + } ), }); if (routeState && routeState.onSaveNavigateTo) { navigateToApp(...routeState.onSaveNavigateTo); } - } else { + }) + .catch((err) => { toasts.addDanger({ + 'data-test-subj': 'policyDetailsFailureMessage', title: i18n.translate('xpack.securitySolution.endpoint.policy.details.updateErrorTitle', { defaultMessage: 'Failed!', }), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - text: policyUpdateStatus.error!.message, + text: err.message, }); - } - } - }, [navigateToApp, toasts, policyName, policyUpdateStatus, routeState, theme.theme$]); - - const handleCancelOnClick = useNavigateToAppEventHandler(...navigateToAppArguments); - - const handleSaveOnClick = useCallback(() => { - setShowConfirm(true); - }, []); - - const handleSaveConfirmation = useCallback(() => { - dispatch({ - type: 'userClickedPolicyDetailsSaveButton', - }); - setShowConfirm(false); - }, [dispatch]); - - const handleSaveCancel = useCallback(() => { - setShowConfirm(false); - }, []); + }); + + handleSaveCancel(); + }, [ + handleSaveCancel, + navigateToApp, + policy, + policyName, + policySettings, + routeState, + sendPolicyUpdate, + toasts, + ]); useEffect(() => { if (!routeState && locationRouteState) { @@ -137,28 +139,25 @@ export const PolicyFormLayout = React.memo(() => { } }, [locationRouteState, routeState]); - // Before proceeding - check if we have a policy data. - // If not, and we are still loading, show spinner. - // Else, if we have an error, then show error on the page. - if (!policyItem) { - return ( - - {isPolicyLoading ? : null} - - ); - } - return ( <> {showConfirm && ( )} - + + + + @@ -173,14 +172,14 @@ export const PolicyFormLayout = React.memo(() => { /> - {showEditableFormFields && ( + {isEditMode && ( { ); }); - -PolicyFormLayout.displayName = 'PolicyFormLayout'; +PolicySettingsLayout.displayName = 'PolicySettingsLayout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx index e16d5fcb392a8..d3f9edc202acc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback, useEffect, useMemo } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; +import { PolicySettingsLayout } from '../policy_settings_layout'; import { useUserPrivileges } from '../../../../../common/components/user_privileges'; import { getPolicyDetailPath, @@ -36,7 +37,6 @@ import { policyIdFromParams, } from '../../store/policy_details/selectors'; import { PolicyArtifactsLayout } from '../artifacts/layout/policy_artifacts_layout'; -import { PolicyFormLayout } from '../policy_forms/components'; import { usePolicyDetailsSelector } from '../policy_hooks'; import { POLICY_ARTIFACT_EVENT_FILTERS_LABELS } from './event_filters_translations'; import { POLICY_ARTIFACT_TRUSTED_APPS_LABELS } from './trusted_apps_translations'; @@ -77,7 +77,11 @@ export const PolicyTabs = React.memo(() => { const isInHostIsolationExceptionsTab = usePolicyDetailsSelector(isOnHostIsolationExceptionsView); const isInBlocklistsTab = usePolicyDetailsSelector(isOnBlocklistsView); const policyId = usePolicyDetailsSelector(policyIdFromParams); - const policyItem = usePolicyDetailsSelector(policyDetails); + + // By the time the tabs load, we know that we already have a `policyItem` since a conditional + // check is done at the `PageDetails` component level. So asserting to non-null/undefined here. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const policyItem = usePolicyDetailsSelector(policyDetails)!; const { canReadTrustedApplications, canWriteTrustedApplications, @@ -195,7 +199,8 @@ export const PolicyTabs = React.memo(() => { content: ( <> - + + ), }, diff --git a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts index a979d79db5663..0c6f4e7db9b89 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts @@ -15,6 +15,7 @@ import type { GetPolicyListResponse } from '../../pages/policy/types'; import { sendGetEndpointSpecificPackagePolicies } from './policies'; import type { ServerApiError } from '../../../common/types'; +// FIXME:PT move to `hooks` folder export function useGetEndpointSpecificPolicies( { onError, diff --git a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts index c6240c934ec8f..e5347449d4dbe 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts @@ -8,8 +8,6 @@ import type { HttpFetchOptions, HttpStart } from '@kbn/core/public'; import type { GetAgentStatusResponse, - GetAgentPoliciesRequest, - GetAgentPoliciesResponse, GetPackagePoliciesResponse, GetInfoResponse, } from '@kbn/fleet-plugin/common'; @@ -59,38 +57,6 @@ export const sendBulkGetPackagePolicies = ( }); }; -/** - * Retrieve a list of Agent Policies - * @param http - * @param options - */ -export const sendGetAgentPolicyList = ( - http: HttpStart, - options: HttpFetchOptions & GetAgentPoliciesRequest -) => { - return http.get(INGEST_API_AGENT_POLICIES, options); -}; - -/** - * Retrieve a list of Agent Policies - * @param http - * @param options - */ -export const sendBulkGetAgentPolicyList = ( - http: HttpStart, - ids: string[], - options: HttpFetchOptions = {} -) => { - return http.post(`${INGEST_API_AGENT_POLICIES}/_bulk_get`, { - ...options, - body: JSON.stringify({ - ids, - ignoreMissing: true, - full: true, - }), - }); -}; - /** * Updates a package policy * diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index dce1d4c5a02a2..abeb8e76b2f8a 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -28955,7 +28955,6 @@ "xpack.securitySolution.endpoint.list.totalCount": "Affichage de {totalItemCount, plural, one {# point de terminaison} many {# points de terminaison} other {# points de terminaison}}", "xpack.securitySolution.endpoint.list.totalCount.limited": "Affichage de {limit} sur {totalItemCount, plural, one {# point de terminaison} many {# points de terminaison} other {# points de terminaison}}", "xpack.securitySolution.endpoint.list.transformFailed.message": "Une transformation requise, {transformId}, est actuellement en échec. La plupart du temps, ce problème peut être corrigé par {transformsPage}. Pour une assistance supplémentaire, veuillez visitez la {docsPage}", - "xpack.securitySolution.endpoint.policy.advanced.show": "Paramètres avancés pour {action}", "xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.backButtonLabel": "Retour à la politique {policyName}", "xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.content": "Aucun artefact n'est actuellement affecté à {policyName}. Affectez des artefacts maintenant, ou ajoutez-les et gérez-les sur la page des artefacts.", "xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.noPrivileges.content": "Aucun artefact n'est actuellement affecté à {policyName}.", @@ -31590,7 +31589,6 @@ "xpack.securitySolution.endpoint.policy.blocklist.list.search.placeholder": "Rechercher sur les champs ci-dessous : nom, description, valeur", "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation": "Activez la bascule pour enregistrer Elastic comme solution d'antivirus officielle pour le système d'exploitation Windows. Cela désactivera également Windows Defender.", "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.osRestriction": "Restrictions", - "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.toggle": "Enregistrer comme antivirus", "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type": "Enregistrer comme antivirus", "xpack.securitySolution.endpoint.policy.details.attack_surface_reduction": "Réduction de la surface d’attaque", "xpack.securitySolution.endpoint.policy.details.attackSurfaceReduction.type": "Réduction de la surface d’attaque", @@ -31600,7 +31598,6 @@ "xpack.securitySolution.endpoint.policy.details.behavior_protection": "Comportement malveillant", "xpack.securitySolution.endpoint.policy.details.cancel": "Annuler", "xpack.securitySolution.endpoint.policy.details.cloudDeploymentLInk": "déploiement sur le cloud", - "xpack.securitySolution.endpoint.policy.details.credentialHardening.toggle": "Renforcement de l’identification", "xpack.securitySolution.endpoint.policy.details.detect": "Détecter", "xpack.securitySolution.endpoint.policy.details.detectionRulesLink": "règles de détection associées", "xpack.securitySolution.endpoint.policy.details.eventCollection": "Collection d'événements", @@ -31668,7 +31665,6 @@ "xpack.securitySolution.endpoint.policy.multiStepOnboarding.learnMore": "En savoir plus", "xpack.securitySolution.endpoint.policy.multiStepOnboarding.title": "Nous enregistrerons votre intégration avec nos valeurs par défaut recommandées.", "xpack.securitySolution.endpoint.policy.protections.behavior": "Protections contre les comportements malveillants", - "xpack.securitySolution.endpoint.policy.protections.blocklist": "Liste noire activée", "xpack.securitySolution.endpoint.policy.protections.malware": "Protections contre les malware", "xpack.securitySolution.endpoint.policy.protections.memory": "Protections de la mémoire contre les menaces", "xpack.securitySolution.endpoint.policy.protections.ransomware": "Protections contre les ransomware", @@ -31701,7 +31697,6 @@ "xpack.securitySolution.endpoint.policyDetails.agentsSummary.onlineTitle": "Intègre", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.totalTitle": "Total des agents", "xpack.securitySolution.endpoint.policyDetails.artifacts.title": "Artefacts", - "xpack.securitySolution.endpoint.policyDetails.loadError": "Impossible de charger les paramètres de la politique des points de terminaison", "xpack.securitySolution.endpoint.policyDetails.settings.title": "Paramètres de politique", "xpack.securitySolution.endpoint.policyDetails.userNotification.placeholder": "Saisir votre message de notification personnalisé", "xpack.securitySolution.endpoint.policyDetailsConfig.blocklistTooltip": "Active ou désactive la liste noire associée à cette politique. La liste noire est une collection de hachages, de chemins ou de signataires qui étend la liste de processus considérés comme malveillants par le point de terminaison. Consultez l'onglet de la liste noire pour plus de détails sur l'entrée.", @@ -31710,10 +31705,8 @@ "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file": "Fichier", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network": "Réseau", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.process": "Processus", - "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.session_data": "Collecter les données de session", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.session_data.description": "Activez cette option pour capturer les données de processus étendues requises pour la vue de session. La vue de session vous fournit une représentation visuelle des données de session et d'exécution du processus. Les données de la vue de session sont organisées en fonction du modèle de processus Linux pour vous aider à examiner l'activité des processus, des utilisateurs et des services dans votre infrastructure Linux.", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.session_data.title": "Données de session", - "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.tty_io": "Capturer la sortie du terminal", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.tty_io.tooltip": "Activez cette option pour collecter la sortie du terminal (tty). La sortie du terminal apparaît dans la vue de session, et vous pouvez l'afficher séparément pour voir quelles commandes ont été exécutées et comment elles ont été tapées, à condition que le terminal soit en mode écho. Fonctionne uniquement sur les hôtes qui prennent en charge ebpf.", "xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.file": "Fichier", "xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.network": "Réseau", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a17cd63a4c62c..b996b954c9af4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -28940,7 +28940,6 @@ "xpack.securitySolution.endpoint.list.totalCount": "{totalItemCount, plural, other {#個のエンドポイント}}を表示中", "xpack.securitySolution.endpoint.list.totalCount.limited": "{limit}/{totalItemCount, plural, other {#個のエンドポイント}}ページを表示中", "xpack.securitySolution.endpoint.list.transformFailed.message": "現在、必須のトランスフォーム{transformId}が失敗しています。通常、これは{transformsPage}で修正できます。ヘルプについては、{docsPage}ご覧ください", - "xpack.securitySolution.endpoint.policy.advanced.show": "{action}高度な設定", "xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.backButtonLabel": "{policyName}ポリシーに戻る", "xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.content": "現在、{policyName}に割り当てられたアーティファクトがありません。今すぐアーティファクトを割り当てるか、アーティファクトページでアーティファクトを追加および管理してください。", "xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.noPrivileges.content": "現在、{policyName}に割り当てられたアーティファクトがありません。", @@ -31575,7 +31574,6 @@ "xpack.securitySolution.endpoint.policy.blocklist.list.search.placeholder": "次のフィールドで検索:名前、説明、値", "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation": "オンにすると、ElasticをWindows OSのオフィシャルウイルス対策ソリューションとして登録します。これで Windows Defender も無効になります。", "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.osRestriction": "制限事項", - "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.toggle": "ウイルス対策として登録", "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type": "ウイルス対策として登録", "xpack.securitySolution.endpoint.policy.details.attack_surface_reduction": "攻撃面削減", "xpack.securitySolution.endpoint.policy.details.attackSurfaceReduction.type": "攻撃面削減", @@ -31585,7 +31583,6 @@ "xpack.securitySolution.endpoint.policy.details.behavior_protection": "悪意のある動作", "xpack.securitySolution.endpoint.policy.details.cancel": "キャンセル", "xpack.securitySolution.endpoint.policy.details.cloudDeploymentLInk": "クラウド展開", - "xpack.securitySolution.endpoint.policy.details.credentialHardening.toggle": "資格情報強化", "xpack.securitySolution.endpoint.policy.details.detect": "検知", "xpack.securitySolution.endpoint.policy.details.detectionRulesLink": "関連する検出ルール", "xpack.securitySolution.endpoint.policy.details.eventCollection": "イベント収集", @@ -31653,7 +31650,6 @@ "xpack.securitySolution.endpoint.policy.multiStepOnboarding.learnMore": "詳細", "xpack.securitySolution.endpoint.policy.multiStepOnboarding.title": "推奨のデフォルト値で統合が保存されます。", "xpack.securitySolution.endpoint.policy.protections.behavior": "悪意ある動作に対する保護", - "xpack.securitySolution.endpoint.policy.protections.blocklist": "ブロックリストが有効にされました", "xpack.securitySolution.endpoint.policy.protections.malware": "マルウェア保護", "xpack.securitySolution.endpoint.policy.protections.memory": "メモリ脅威に対する保護", "xpack.securitySolution.endpoint.policy.protections.ransomware": "ランサムウェア保護", @@ -31686,7 +31682,6 @@ "xpack.securitySolution.endpoint.policyDetails.agentsSummary.onlineTitle": "正常", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.totalTitle": "合計エージェント数", "xpack.securitySolution.endpoint.policyDetails.artifacts.title": "アーチファクト", - "xpack.securitySolution.endpoint.policyDetails.loadError": "エンドポイントポリシー設定を読み込めませんでした", "xpack.securitySolution.endpoint.policyDetails.settings.title": "ポリシー設定", "xpack.securitySolution.endpoint.policyDetails.userNotification.placeholder": "カスタム通知メッセージを入力", "xpack.securitySolution.endpoint.policyDetailsConfig.blocklistTooltip": "このポリシーに関連付けられたブロックリストを有効または無効にします。このブロックリストは、コレクションハッシュ、パス、または署名者です。これはエンドポイントによって悪意があると見なされるプロセスのリストを拡張します。エントリ詳細については、ブロックリストタブを参照してください。", @@ -31695,10 +31690,8 @@ "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file": "ファイル", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network": "ネットワーク", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.process": "プロセス", - "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.session_data": "セッションデータを収集", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.session_data.description": "オンにすると、セッションビューに必要な拡張プロセスデータを取り込みます。セッションビューでは、セッションおよびプロセス実行データが視覚的に表示されます。セッションビューデータは、Linuxプロセスモデルに従って整理して表示され、Linuxインフラストラクチャーのプロセス、ユーザー、サービスアクティビティを調査できます。", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.session_data.title": "セッションデータ", - "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.tty_io": "ターミナル出力を取り込む", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.tty_io.tooltip": "オンにすると、ターミナル(tty)出力を収集します。ターミナル出力はセッションビューに表示されます。ターミナルがエコーモードの場合は、実行されたコマンド、入力方法を個別に表示して確認できます。ebpfをサポートするホストでのみ動作します。", "xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.file": "ファイル", "xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.network": "ネットワーク", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f8262933b3a83..30c37966d9de6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -28936,7 +28936,6 @@ "xpack.securitySolution.endpoint.list.totalCount": "正在显示 {totalItemCount, plural, other {# 个终端}}", "xpack.securitySolution.endpoint.list.totalCount.limited": "正在显示 {limit} 个,共 {totalItemCount, plural, other {# 个终端}} 个", "xpack.securitySolution.endpoint.list.transformFailed.message": "所需的转换 {transformId} 当前失败。多数时候,这可以通过 {transformsPage} 解决。要获取更多帮助,请访问{docsPage}", - "xpack.securitySolution.endpoint.policy.advanced.show": "{action} 高级设置", "xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.backButtonLabel": "返回到 {policyName} 策略", "xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.content": "当前没有项目已分配给 {policyName}。立即分配项目,或在项目页面上添加和管理项目。", "xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.noPrivileges.content": "当前没有项目已分配给 {policyName}。", @@ -31571,7 +31570,6 @@ "xpack.securitySolution.endpoint.policy.blocklist.list.search.placeholder": "搜索下面的字段:name、description、value", "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation": "打开可将 Elastic 注册为 Windows 操作系统的正式防病毒解决方案。这也将禁用 Windows Defender。", "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.osRestriction": "限制", - "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.toggle": "注册为防病毒解决方案", "xpack.securitySolution.endpoint.policy.details.antivirusRegistration.type": "注册为防病毒解决方案", "xpack.securitySolution.endpoint.policy.details.attack_surface_reduction": "攻击面减少", "xpack.securitySolution.endpoint.policy.details.attackSurfaceReduction.type": "攻击面减少", @@ -31581,7 +31579,6 @@ "xpack.securitySolution.endpoint.policy.details.behavior_protection": "恶意行为", "xpack.securitySolution.endpoint.policy.details.cancel": "取消", "xpack.securitySolution.endpoint.policy.details.cloudDeploymentLInk": "云部署", - "xpack.securitySolution.endpoint.policy.details.credentialHardening.toggle": "凭据强化", "xpack.securitySolution.endpoint.policy.details.detect": "检测", "xpack.securitySolution.endpoint.policy.details.detectionRulesLink": "相关检测规则", "xpack.securitySolution.endpoint.policy.details.eventCollection": "事件收集", @@ -31649,7 +31646,6 @@ "xpack.securitySolution.endpoint.policy.multiStepOnboarding.learnMore": "了解详情", "xpack.securitySolution.endpoint.policy.multiStepOnboarding.title": "我们将使用建议的默认值保存您的集成。", "xpack.securitySolution.endpoint.policy.protections.behavior": "恶意行为防护", - "xpack.securitySolution.endpoint.policy.protections.blocklist": "阻止列表已启用", "xpack.securitySolution.endpoint.policy.protections.malware": "恶意软件防护", "xpack.securitySolution.endpoint.policy.protections.memory": "内存威胁防护", "xpack.securitySolution.endpoint.policy.protections.ransomware": "勒索软件防护", @@ -31682,7 +31678,6 @@ "xpack.securitySolution.endpoint.policyDetails.agentsSummary.onlineTitle": "运行正常", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.totalTitle": "代理总数", "xpack.securitySolution.endpoint.policyDetails.artifacts.title": "项目", - "xpack.securitySolution.endpoint.policyDetails.loadError": "无法加载终端策略设置", "xpack.securitySolution.endpoint.policyDetails.settings.title": "策略设置", "xpack.securitySolution.endpoint.policyDetails.userNotification.placeholder": "输入您的定制通知消息", "xpack.securitySolution.endpoint.policyDetailsConfig.blocklistTooltip": "启用或禁用与此策略关联的阻止列表。阻止列表是哈希、路径或签名者的集合,它扩充了终端视为恶意的进程列表。查看阻止列表选项卡了解条目详情。", @@ -31691,10 +31686,8 @@ "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.file": "文件", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.network": "网络", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.process": "进程", - "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.session_data": "收集会话数据", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.session_data.description": "打开此项可捕获会话视图所需的扩展进程数据。会话视图为您提供了会话和进程执行数据的视觉表示形式。会话视图数据将根据 Linux 进程模型进行组织,以帮助您调查 Linux 基础架构上的进程、用户和服务活动。", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.session_data.title": "会话数据", - "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.tty_io": "捕获终端输出", "xpack.securitySolution.endpoint.policyDetailsConfig.linux.events.tty_io.tooltip": "打开此项可收集终端 (tty) 输出。终端输出在会话视图中显示,只要终端处于回显模式,您就可以单独查看该输出来了解执行了哪些命令、如何键入这些命令。仅在支持 ebpf 的主机上运行。", "xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.file": "文件", "xpack.securitySolution.endpoint.policyDetailsConfig.mac.events.network": "网络", diff --git a/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts index e167315e0a126..0093119a21e18 100644 --- a/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts @@ -7,7 +7,8 @@ import expect from '@kbn/expect'; import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; -import { popupVersionsMap } from '@kbn/security-solution-plugin/public/management/pages/policy/view/policy_forms/protections/popup_options_to_versions'; +import { PROTECTION_NOTICE_SUPPORTED_ENDPOINT_VERSION } from '@kbn/security-solution-plugin/public/management/pages/policy/view/policy_settings_form/protection_notice_supported_endpoint_version'; +import { getPolicySettingsFormTestSubjects } from '@kbn/security-solution-plugin/public/management/pages/policy/view/policy_settings_form/mocks'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PolicyTestResourceInfo } from '../../services/endpoint_policy'; @@ -28,6 +29,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('When on the Endpoint Policy Details Page', function () { let indexedData: IndexedHostsAndAlertsResponse; + const formTestSubjects = getPolicySettingsFormTestSubjects(); before(async () => { indexedData = await endpointTestResources.loadEndpointData(); @@ -80,91 +82,94 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.policy.navigateToPolicyDetails(policyInfo.packagePolicy.id); }); - it('and the show advanced settings button is clicked', async () => { - await testSubjects.missingOrFail('advancedPolicyPanel'); + it('Should show/hide advanced section when button is clicked', async () => { + await testSubjects.missingOrFail(formTestSubjects.advancedSection.settingsContainer); // Expand await pageObjects.policy.showAdvancedSettingsSection(); - await testSubjects.existOrFail('advancedPolicyPanel'); + await testSubjects.existOrFail(formTestSubjects.advancedSection.settingsContainer); // Collapse await pageObjects.policy.hideAdvancedSettingsSection(); - await testSubjects.missingOrFail('advancedPolicyPanel'); + await testSubjects.missingOrFail(formTestSubjects.advancedSection.settingsContainer); }); }); ['malware', 'ransomware'].forEach((protection) => { - describe(`on the ${protection} protections section`, () => { + describe(`on the ${protection} protections card`, () => { let policyInfo: PolicyTestResourceInfo; + const cardTestSubj: + | typeof formTestSubjects['ransomware'] + | typeof formTestSubjects['malware'] = + formTestSubjects[ + protection as keyof Pick + ]; beforeEach(async () => { policyInfo = await policyTestResources.createPolicy(); await pageObjects.policy.navigateToPolicyDetails(policyInfo.packagePolicy.id); - await testSubjects.existOrFail(`${protection}ProtectionsForm`); }); afterEach(async () => { if (policyInfo) { await policyInfo.cleanup(); + + // @ts-expect-error forcing to undefined + policyInfo = undefined; } }); - it('should show the supported Endpoint version', async () => { - const supportedVersionElement = await testSubjects.findDescendant( - 'policySupportedVersions', - await testSubjects.find(`${protection}ProtectionsForm`) - ); - - expect(await supportedVersionElement.getVisibleText()).to.equal( - 'Agent version ' + popupVersionsMap.get(protection) + it('should show the supported Endpoint version for user notification', async () => { + expect(await testSubjects.getVisibleText(cardTestSubj.notifySupportedVersion)).to.equal( + 'Agent version ' + + PROTECTION_NOTICE_SUPPORTED_ENDPOINT_VERSION[ + protection as keyof typeof PROTECTION_NOTICE_SUPPORTED_ENDPOINT_VERSION + ] ); }); it('should show the custom message text area when the Notify User checkbox is checked', async () => { - expect(await testSubjects.isChecked(`${protection}UserNotificationCheckbox`)).to.be(true); - await testSubjects.existOrFail(`${protection}UserNotificationCustomMessage`); + expect(await testSubjects.isChecked(cardTestSubj.notifyUserCheckbox)).to.be(true); + await testSubjects.existOrFail(cardTestSubj.notifyCustomMessage); }); it('should not show the custom message text area when the Notify User checkbox is unchecked', async () => { - await pageObjects.endpointPageUtils.clickOnEuiCheckbox( - `${protection}UserNotificationCheckbox` - ); - expect(await testSubjects.isChecked(`${protection}UserNotificationCheckbox`)).to.be( - false - ); - await testSubjects.missingOrFail(`${protection}UserNotificationCustomMessage`); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox(cardTestSubj.notifyUserCheckbox); + expect(await testSubjects.isChecked(cardTestSubj.notifyUserCheckbox)).to.be(false); + await testSubjects.missingOrFail(cardTestSubj.notifyCustomMessage); }); it('should show a sample custom message', async () => { - const customMessageBox = await testSubjects.find( - `${protection}UserNotificationCustomMessage` - ); - expect(await customMessageBox.getVisibleText()).equal( + expect(await testSubjects.getVisibleText(cardTestSubj.notifyCustomMessage)).equal( 'Elastic Security {action} {filename}' ); }); - it('should show a tooltip ', async () => { - const malwareTooltipIcon = await testSubjects.find(`${protection}TooltipIcon`); - await malwareTooltipIcon.moveMouseTo(); + it('should show a tooltip on hover', async () => { + await testSubjects.moveMouseTo(cardTestSubj.notifyCustomMessageTooltipIcon); - const malwareTooltip = await testSubjects.find(`${protection}Tooltip`); - expect(await malwareTooltip.getVisibleText()).equal( + expect( + await testSubjects.getVisibleText(cardTestSubj.notifyCustomMessageTooltipInfo) + ).equal( `Selecting the user notification option will display a notification to the host user when ${protection} is prevented or detected.\nThe user notification can be customized in the text box below. Bracketed tags can be used to dynamically populate the applicable action (such as prevented or detected) and the filename.` ); }); it('should preserve a custom notification message upon saving', async () => { - const customMessageBox = await testSubjects.find( - `${protection}UserNotificationCustomMessage` + await testSubjects.setValue(cardTestSubj.notifyCustomMessage, '', { + clearWithKeyboard: true, + }); + await testSubjects.setValue( + cardTestSubj.notifyCustomMessage, + 'a custom notification message @$% 123', + { typeCharByChar: true } ); - await customMessageBox.clearValue(); - await customMessageBox.type('a custom notification message @$% 123'); + await pageObjects.policy.confirmAndSave(); await testSubjects.existOrFail('policyDetailsSuccessMessage'); - expect( - await testSubjects.getVisibleText(`${protection}UserNotificationCustomMessage`) - ).to.equal('a custom notification message @$% 123'); + expect(await testSubjects.getVisibleText(cardTestSubj.notifyCustomMessage)).to.equal( + 'a custom notification message @$% 123' + ); }); }); }); @@ -180,21 +185,28 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { afterEach(async () => { if (policyInfo) { await policyInfo.cleanup(); + + // @ts-expect-error forcing to undefined + policyInfo = undefined; } }); it('should display success toast on successful save', async () => { - await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox( + formTestSubjects.windowsEvents.dnsCheckbox + ); await pageObjects.policy.confirmAndSave(); await testSubjects.existOrFail('policyDetailsSuccessMessage'); expect(await testSubjects.getVisibleText('policyDetailsSuccessMessage')).to.equal( - `Integration ${policyInfo.packagePolicy.name} has been updated.` + `Success!\nIntegration ${policyInfo.packagePolicy.name} has been updated.` ); }); it('should persist update on the screen', async () => { - await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_process'); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox( + formTestSubjects.windowsEvents.processCheckbox + ); await pageObjects.policy.confirmAndSave(); await testSubjects.existOrFail('policyDetailsSuccessMessage'); @@ -202,9 +214,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.endpoint.navigateToEndpointList(); await pageObjects.policy.navigateToPolicyDetails(policyInfo.packagePolicy.id); - expect(await (await testSubjects.find('policyWindowsEvent_process')).isSelected()).to.equal( - false - ); + expect( + await ( + await testSubjects.find(formTestSubjects.windowsEvents.processCheckbox) + ).isSelected() + ).to.equal(false); }); it('should have updated policy data in overall Agent Policy', async () => { @@ -212,9 +226,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // to the generated Agent Policy that is dispatch down to the Elastic Agent. await Promise.all([ - pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_file'), - pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyLinuxEvent_file'), - pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyMacEvent_file'), + pageObjects.endpointPageUtils.clickOnEuiCheckbox( + formTestSubjects.windowsEvents.fileCheckbox + ), + pageObjects.endpointPageUtils.clickOnEuiCheckbox( + formTestSubjects.linuxEvents.fileCheckbox + ), + pageObjects.endpointPageUtils.clickOnEuiCheckbox(formTestSubjects.macEvents.fileCheckbox), ]); await pageObjects.policy.showAdvancedSettingsSection(); @@ -290,7 +308,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { policyInfo.agentPolicy.id, policyInfo.packagePolicy.id ); - await testSubjects.existOrFail('endpointIntegrationPolicyForm'); }); afterEach(async () => { @@ -300,27 +317,41 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should show the endpoint policy form', async () => { - await testSubjects.existOrFail('endpointIntegrationPolicyForm'); + await testSubjects.existOrFail(formTestSubjects.form); }); it('should allow updates to policy items', async () => { - const winDnsEventingCheckbox = await testSubjects.find('policyWindowsEvent_dns'); + const winDnsEventingCheckbox = await testSubjects.find( + formTestSubjects.windowsEvents.dnsCheckbox + ); await pageObjects.ingestManagerCreatePackagePolicy.scrollToCenterOfWindow( winDnsEventingCheckbox ); expect(await winDnsEventingCheckbox.isSelected()).to.be(true); - await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); - await pageObjects.policy.waitForCheckboxSelectionChange('policyWindowsEvent_dns', false); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox( + formTestSubjects.windowsEvents.dnsCheckbox + ); + await pageObjects.policy.waitForCheckboxSelectionChange( + formTestSubjects.windowsEvents.dnsCheckbox, + false + ); }); it('should include updated endpoint data when saved', async () => { await pageObjects.ingestManagerCreatePackagePolicy.scrollToCenterOfWindow( - await testSubjects.find('policyWindowsEvent_dns') + await testSubjects.find(formTestSubjects.windowsEvents.dnsCheckbox) + ); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox( + formTestSubjects.windowsEvents.dnsCheckbox + ); + const updatedCheckboxValue = await testSubjects.isSelected( + formTestSubjects.windowsEvents.dnsCheckbox ); - await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); - const updatedCheckboxValue = await testSubjects.isSelected('policyWindowsEvent_dns'); - await pageObjects.policy.waitForCheckboxSelectionChange('policyWindowsEvent_dns', false); + await pageObjects.policy.waitForCheckboxSelectionChange( + formTestSubjects.windowsEvents.dnsCheckbox, + false + ); await (await pageObjects.ingestManagerCreatePackagePolicy.findSaveButton(true)).click(); await pageObjects.ingestManagerCreatePackagePolicy.waitForSaveSuccessNotification(true); @@ -331,7 +362,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); await pageObjects.policy.waitForCheckboxSelectionChange( - 'policyWindowsEvent_dns', + formTestSubjects.windowsEvents.dnsCheckbox, updatedCheckboxValue ); }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index da4fb936d6655..f2b6452245135 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -6,12 +6,14 @@ */ import expect from '@kbn/expect'; +import { getPolicySettingsFormTestSubjects } from '@kbn/security-solution-plugin/public/management/pages/policy/view/policy_settings_form/mocks'; import { FtrProviderContext } from '../ftr_provider_context'; export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'header']); const testSubjects = getService('testSubjects'); const retryService = getService('retry'); + const formTestSubj = getPolicySettingsFormTestSubjects(); return { /** @@ -59,16 +61,8 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr return await testSubjects.find('policyDetailsCancelButton'); }, - /** - * Finds and returns the Advanced Policy Show/Hide Button - */ - async findAdvancedPolicyButton() { - await this.ensureIsOnDetailsPage(); - return await testSubjects.find('advancedPolicyButton'); - }, - async isAdvancedSettingsExpanded() { - return await testSubjects.exists('advancedPolicyPanel'); + return await testSubjects.exists(formTestSubj.advancedSection.settingsContainer); }, /** @@ -76,12 +70,9 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr */ async showAdvancedSettingsSection() { if (!(await this.isAdvancedSettingsExpanded())) { - const expandButton = await this.findAdvancedPolicyButton(); - await expandButton.click(); + await testSubjects.click(formTestSubj.advancedSection.showHideButton); } - - await testSubjects.existOrFail('advancedPolicyPanel'); - await testSubjects.scrollIntoView('advancedPolicyPanel'); + await testSubjects.scrollIntoView(formTestSubj.advancedSection.settingsContainer); }, /** @@ -89,10 +80,9 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr */ async hideAdvancedSettingsSection() { if (await this.isAdvancedSettingsExpanded()) { - const expandButton = await this.findAdvancedPolicyButton(); - await expandButton.click(); + await testSubjects.click(formTestSubj.advancedSection.showHideButton); } - await testSubjects.missingOrFail('advancedPolicyPanel'); + await testSubjects.missingOrFail(formTestSubj.advancedSection.settingsContainer); }, /** @@ -104,7 +94,7 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr }, /** - * ensures that the Details Page is the currently display view + * ensures that the Details Page is currently displayed */ async ensureIsOnDetailsPage() { await testSubjects.existOrFail('policyDetailsPage'); @@ -116,8 +106,6 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr async confirmAndSave() { await this.ensureIsOnDetailsPage(); - const saveButton = await this.findSaveButton(); - // Sometimes, data retrieval errors may have been encountered by other security solution processes // (ex. index fields search here: `x-pack/plugins/security_solution/public/common/containers/source/index.tsx:181`) // which are displayed using one or more Toast messages. This in turn prevents the user from @@ -125,31 +113,11 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr // we'll first check that all toasts are cleared await pageObjects.common.clearAllToasts(); - await saveButton.click(); + await testSubjects.click('policyDetailsSaveButton'); await testSubjects.existOrFail('policyDetailsConfirmModal'); await pageObjects.common.clickConfirmOnModal(); }, - /** - * Finds and returns the Create New policy Policy button displayed on the List page - */ - async findHeaderCreateNewButton() { - // The Create button is initially disabled because we need to first make a call to Ingest - // to retrieve the package version, so that the redirect works as expected. So, we wait - // for that to occur here a well. - await testSubjects.waitForEnabled('headerCreateNewPolicyButton'); - return await testSubjects.find('headerCreateNewPolicyButton'); - }, - - /** - * Used when looking a the Ingest create/edit package policy pages. Finds the endpoint - * custom configuration component - * @param onEditPage - */ - async findPackagePolicyEndpointCustomConfiguration(onEditPage: boolean = false) { - return await testSubjects.find(`endpointPackagePolicy_${onEditPage ? 'edit' : 'create'}`); - }, - /** * Waits for a Checkbox/Radiobutton to have its `isSelected()` value match the provided expected value * @param selector