diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index fbc0d5b2ef269..097a85aea54df 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -109,7 +109,7 @@ describe('checking migration metadata changes on all registered SO types', () => "ingest-agent-policies": "7633e578f60c074f8267bc50ec4763845e431437", "ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d", "ingest-outputs": "daafff49255ab700e07491376fe89f04fc998b91", - "ingest-package-policies": "f4c2767e852b700a8b82678925b86bac08958b43", + "ingest-package-policies": "8a99e165aab00c6c365540427a3abeb7bea03f31", "ingest_manager_settings": "91445219e7115ff0c45d1dabd5d614a80b421797", "inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83", "kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad", diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 0e44db492b2c1..80665f381e871 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -73,6 +73,7 @@ import { } from './migrations/to_v8_6_0'; import { migratePackagePolicyToV8100, + migratePackagePolicyToV8140, migratePackagePolicyToV870, } from './migrations/security_solution'; import { migratePackagePolicyToV880 } from './migrations/to_v8_8_0'; @@ -473,6 +474,14 @@ export const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({ }, ], }, + '6': { + changes: [ + { + type: 'data_backfill', + backfillFn: migratePackagePolicyToV8140, + }, + ], + }, }, migrations: { '7.10.0': migratePackagePolicyToV7100, diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts index 8248c181405e5..cd37ed56b5b58 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts @@ -19,3 +19,4 @@ export { migratePackagePolicyToV860 } from './to_v8_6_0'; export { migratePackagePolicyToV870 } from './to_v8_7_0'; export { migratePackagePolicyToV880 } from './to_v8_8_0'; export { migratePackagePolicyToV8100 } from './to_v8_10_0'; +export { migratePackagePolicyToV8140 } from './to_v8_14_0'; diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.test.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.test.ts new file mode 100644 index 0000000000000..e724c039179dc --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.test.ts @@ -0,0 +1,137 @@ +/* + * 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 { + createModelVersionTestMigrator, + type ModelVersionTestMigrator, +} from '@kbn/core-test-helpers-model-versions'; + +import { cloneDeep } from 'lodash'; + +import type { SavedObject } from '@kbn/core-saved-objects-server'; + +import type { PackagePolicy } from '../../../../common'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common'; +import { getSavedObjectTypes } from '../..'; + +const policyDoc: SavedObject = { + id: 'mock-saved-object-id', + attributes: { + name: 'Some Policy Name', + package: { + name: 'endpoint', + title: '', + version: '', + }, + id: 'endpoint', + policy_id: '', + enabled: true, + namespace: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: { + windows: { + malware: { + mode: 'detect', + blocklist: true, + }, + }, + mac: { + malware: { + mode: 'detect', + blocklist: true, + }, + }, + linux: { + malware: { + mode: 'detect', + blocklist: true, + }, + }, + }, + }, + }, + }, + ], + }, + type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, + references: [], +}; + +describe('8.14.0 Endpoint Package Policy migration', () => { + let migrator: ModelVersionTestMigrator; + + beforeEach(() => { + migrator = createModelVersionTestMigrator({ + type: getSavedObjectTypes()[PACKAGE_POLICY_SAVED_OBJECT_TYPE], + }); + }); + + it('should backfill `on_write_scan` field to malware protections on Kibana update', () => { + const originalPolicyConfigSO = cloneDeep(policyDoc); + + const migratedPolicyConfigSO = migrator.migrate({ + document: originalPolicyConfigSO, + fromVersion: 5, + toVersion: 6, + }); + + const migratedPolicyConfig = migratedPolicyConfigSO.attributes.inputs[0].config?.policy.value; + expect(migratedPolicyConfig.windows.malware.on_write_scan).toBe(true); + expect(migratedPolicyConfig.mac.malware.on_write_scan).toBe(true); + expect(migratedPolicyConfig.linux.malware.on_write_scan).toBe(true); + }); + + it('should not backfill `on_write_scan` field if already present due to user edit before migration is performed on serverless', () => { + const originalPolicyConfigSO = cloneDeep(policyDoc); + const originalPolicyConfig = originalPolicyConfigSO.attributes.inputs[0].config?.policy.value; + originalPolicyConfig.windows.malware.on_write_scan = false; + originalPolicyConfig.mac.malware.on_write_scan = true; + originalPolicyConfig.linux.malware.on_write_scan = false; + + const migratedPolicyConfigSO = migrator.migrate({ + document: originalPolicyConfigSO, + fromVersion: 5, + toVersion: 6, + }); + + const migratedPolicyConfig = migratedPolicyConfigSO.attributes.inputs[0].config?.policy.value; + expect(migratedPolicyConfig.windows.malware.on_write_scan).toBe(false); + expect(migratedPolicyConfig.mac.malware.on_write_scan).toBe(true); + expect(migratedPolicyConfig.linux.malware.on_write_scan).toBe(false); + }); + + // no reason for removing `on_write_scan` for a lower version Kibana - the field will just sit silently in the package config + it('should not strip `on_write_scan` in regards of forward compatibility', () => { + const originalPolicyConfigSO = cloneDeep(policyDoc); + const originalPolicyConfig = originalPolicyConfigSO.attributes.inputs[0].config?.policy.value; + originalPolicyConfig.windows.malware.on_write_scan = false; + originalPolicyConfig.mac.malware.on_write_scan = true; + originalPolicyConfig.linux.malware.on_write_scan = false; + + const migratedPolicyConfigSO = migrator.migrate({ + document: originalPolicyConfigSO, + fromVersion: 6, + toVersion: 5, + }); + + const migratedPolicyConfig = migratedPolicyConfigSO.attributes.inputs[0].config?.policy.value; + expect(migratedPolicyConfig.windows.malware.on_write_scan).toBe(false); + expect(migratedPolicyConfig.mac.malware.on_write_scan).toBe(true); + expect(migratedPolicyConfig.linux.malware.on_write_scan).toBe(false); + }); +}); diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.ts new file mode 100644 index 0000000000000..cc97dafe72180 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v8_14_0.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 { SavedObjectUnsanitizedDoc } from '@kbn/core/server'; + +import type { SavedObjectModelDataBackfillFn } from '@kbn/core-saved-objects-server'; + +import type { PackagePolicy } from '../../../../common'; + +const ON_WRITE_SCAN_DEFAULT_VALUE = true; + +export const migratePackagePolicyToV8140: SavedObjectModelDataBackfillFn< + PackagePolicy, + PackagePolicy +> = (packagePolicyDoc) => { + if (packagePolicyDoc.attributes.package?.name !== 'endpoint') { + return { attributes: packagePolicyDoc.attributes }; + } + + const updatedPackagePolicyDoc: SavedObjectUnsanitizedDoc = packagePolicyDoc; + + const input = updatedPackagePolicyDoc.attributes.inputs[0]; + + if (input && input.config) { + const policy = input.config.policy.value; + + policy.windows.malware.on_write_scan ??= ON_WRITE_SCAN_DEFAULT_VALUE; + policy.mac.malware.on_write_scan ??= ON_WRITE_SCAN_DEFAULT_VALUE; + policy.linux.malware.on_write_scan ??= ON_WRITE_SCAN_DEFAULT_VALUE; + } + + return { attributes: updatedPackagePolicyDoc.attributes }; +}; diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index 5b16316d9baaa..b99fb0cce0985 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -104,5 +104,6 @@ "@kbn/config", "@kbn/core-http-server-mocks", "@kbn/code-editor", + "@kbn/core-test-helpers-model-versions", ] } diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts index 91585c9b16fa1..779a309e03d32 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -43,6 +43,7 @@ export const policyFactory = ( malware: { mode: ProtectionModes.prevent, blocklist: true, + on_write_scan: true, }, ransomware: { mode: ProtectionModes.prevent, @@ -96,6 +97,7 @@ export const policyFactory = ( malware: { mode: ProtectionModes.prevent, blocklist: true, + on_write_scan: true, }, behavior_protection: { mode: ProtectionModes.prevent, @@ -138,6 +140,7 @@ export const policyFactory = ( malware: { mode: ProtectionModes.prevent, blocklist: true, + on_write_scan: true, }, behavior_protection: { mode: ProtectionModes.prevent, diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts index 8e1c4e087c827..662c47f9c8999 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts @@ -212,7 +212,7 @@ export const eventsOnlyPolicy = (): PolicyConfig => ({ registry: true, security: true, }, - malware: { mode: ProtectionModes.off, blocklist: false }, + malware: { mode: ProtectionModes.off, blocklist: false, on_write_scan: false }, ransomware: { mode: ProtectionModes.off, supported: true }, memory_protection: { mode: ProtectionModes.off, supported: true }, behavior_protection: { mode: ProtectionModes.off, supported: true, reputation_service: false }, @@ -228,7 +228,7 @@ export const eventsOnlyPolicy = (): PolicyConfig => ({ }, mac: { events: { process: true, file: true, network: true }, - malware: { mode: ProtectionModes.off, blocklist: false }, + malware: { mode: ProtectionModes.off, blocklist: false, on_write_scan: false }, behavior_protection: { mode: ProtectionModes.off, supported: true, reputation_service: false }, memory_protection: { mode: ProtectionModes.off, supported: true }, popup: { @@ -249,7 +249,7 @@ export const eventsOnlyPolicy = (): PolicyConfig => ({ session_data: false, tty_io: false, }, - malware: { mode: ProtectionModes.off, blocklist: false }, + malware: { mode: ProtectionModes.off, blocklist: false, on_write_scan: false }, behavior_protection: { mode: ProtectionModes.off, supported: true, reputation_service: false }, memory_protection: { mode: ProtectionModes.off, supported: true }, popup: { diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts index 472fcfdfd825a..c6ffc43928bd6 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.ts @@ -103,7 +103,10 @@ const disableCommonProtections = (policy: PolicyConfig) => { }, policy); }; -const getDisabledCommonProtectionsForOS = (policy: PolicyConfig, os: PolicyOperatingSystem) => ({ +const getDisabledCommonProtectionsForOS = ( + policy: PolicyConfig, + os: PolicyOperatingSystem +): Partial => ({ behavior_protection: { ...policy[os].behavior_protection, mode: ProtectionModes.off, @@ -115,6 +118,7 @@ const getDisabledCommonProtectionsForOS = (policy: PolicyConfig, os: PolicyOpera malware: { ...policy[os].malware, blocklist: false, + on_write_scan: false, mode: ProtectionModes.off, }, }); diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index f525c03ce17f7..68867e92d7294 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -973,7 +973,7 @@ export interface PolicyConfig { registry: boolean; security: boolean; }; - malware: ProtectionFields & BlocklistFields; + malware: ProtectionFields & BlocklistFields & OnWriteScanFields; memory_protection: ProtectionFields & SupportedFields; behavior_protection: BehaviorProtectionFields & SupportedFields; ransomware: ProtectionFields & SupportedFields; @@ -1014,7 +1014,7 @@ export interface PolicyConfig { process: boolean; network: boolean; }; - malware: ProtectionFields & BlocklistFields; + malware: ProtectionFields & BlocklistFields & OnWriteScanFields; behavior_protection: BehaviorProtectionFields & SupportedFields; memory_protection: ProtectionFields & SupportedFields; popup: { @@ -1044,7 +1044,7 @@ export interface PolicyConfig { session_data: boolean; tty_io: boolean; }; - malware: ProtectionFields & BlocklistFields; + malware: ProtectionFields & BlocklistFields & OnWriteScanFields; behavior_protection: BehaviorProtectionFields & SupportedFields; memory_protection: ProtectionFields & SupportedFields; popup: { @@ -1120,6 +1120,10 @@ export interface BlocklistFields { blocklist: boolean; } +export interface OnWriteScanFields { + on_write_scan?: boolean; +} + /** Policy protection mode options */ export enum ProtectionModes { detect = 'detect', diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index a6eba0197b490..72ea5b723758c 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -228,6 +228,11 @@ export const allowedExperimentalValues = Object.freeze({ * Expires: on Apr 23, 2024 */ perFieldPrebuiltRulesDiffingEnabled: true, + + /** + * Makes Elastic Defend integration's Malware On-Write Scan option available to edit. + */ + malwareOnWriteScanOptionAvailable: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index 1c7a8e0b18a61..d93b2aa6a1e39 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -12,7 +12,7 @@ import type { PolicyDetailsAction } from '.'; import { policyDetailsReducer, policyDetailsMiddlewareFactory } from '.'; import { policyConfig } from './selectors'; import { policyFactory } from '../../../../../../common/endpoint/models/policy_config'; -import type { PolicyData } from '../../../../../../common/endpoint/types'; +import type { PolicyConfig, PolicyData } from '../../../../../../common/endpoint/types'; import type { MiddlewareActionSpyHelper } from '../../../../../common/store/test_utils'; import { createSpyMiddleware } from '../../../../../common/store/test_utils'; import type { AppContextTestRender } from '../../../../../common/mock/endpoint'; @@ -289,7 +289,7 @@ describe('policy details: ', () => { registry: true, security: true, }, - malware: { mode: 'prevent', blocklist: true }, + malware: { mode: 'prevent', blocklist: true, on_write_scan: true }, memory_protection: { mode: 'off', supported: false }, behavior_protection: { mode: 'off', @@ -327,7 +327,7 @@ describe('policy details: ', () => { }, mac: { events: { process: true, file: true, network: true }, - malware: { mode: 'prevent', blocklist: true }, + malware: { mode: 'prevent', blocklist: true, on_write_scan: true }, behavior_protection: { mode: 'off', supported: false, @@ -363,7 +363,7 @@ describe('policy details: ', () => { tty_io: false, }, logging: { file: 'info' }, - malware: { mode: 'prevent', blocklist: true }, + malware: { mode: 'prevent', blocklist: true, on_write_scan: true }, behavior_protection: { mode: 'off', supported: false, @@ -388,7 +388,7 @@ describe('policy details: ', () => { capture_env_vars: 'LD_PRELOAD,LD_LIBRARY_PATH', }, }, - }, + } as PolicyConfig, }, }, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.test.tsx index f300622231c05..516b5dc835644 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/cards/malware_protections_card.test.tsx @@ -17,6 +17,7 @@ import { FleetPackagePolicyGenerator } from '../../../../../../../../common/endp import React from 'react'; import type { MalwareProtectionsProps } from './malware_protections_card'; import { MalwareProtectionsCard } from './malware_protections_card'; +import type { PolicyConfig } from '../../../../../../../../common/endpoint/types'; import { ProtectionModes } from '../../../../../../../../common/endpoint/types'; import { cloneDeep, set } from 'lodash'; import userEvent from '@testing-library/user-event'; @@ -27,11 +28,12 @@ describe('Policy Malware Protections Card', () => { const testSubj = getPolicySettingsFormTestSubjects('test').malware; let formProps: MalwareProtectionsProps; - let render: () => ReturnType; + let render: (policyConfig?: PolicyConfig) => ReturnType; let renderResult: ReturnType; beforeEach(() => { const mockedContext = createAppRootMockRenderer(); + mockedContext.setExperimentalFlag({ malwareOnWriteScanOptionAvailable: true }); formProps = { policy: new FleetPackagePolicyGenerator('seed').generateEndpointPackagePolicy().inputs[0] @@ -41,7 +43,10 @@ describe('Policy Malware Protections Card', () => { 'data-test-subj': testSubj.card, }; - render = () => (renderResult = mockedContext.render()); + render = (policyConfig = formProps.policy) => + (renderResult = mockedContext.render( + + )); }); it('should render the card with expected components', () => { @@ -60,48 +65,72 @@ describe('Policy Malware Protections Card', () => { ); }); - it('should set Blocklist to disabled if malware is turned off', () => { - const expectedUpdatedPolicy = cloneDeep(formProps.policy); - setMalwareMode(expectedUpdatedPolicy, true); - render(); - userEvent.click(renderResult.getByTestId(testSubj.enableDisableSwitch)); - - expect(formProps.onChange).toHaveBeenCalledWith({ - isValid: true, - updatedPolicy: expectedUpdatedPolicy, - }); - }); - - it('should allow blocklist to be disabled', () => { - const expectedUpdatedPolicy = cloneDeep(formProps.policy); - set(expectedUpdatedPolicy, 'windows.malware.blocklist', false); - set(expectedUpdatedPolicy, 'mac.malware.blocklist', false); - set(expectedUpdatedPolicy, 'linux.malware.blocklist', false); - render(); - userEvent.click(renderResult.getByTestId(testSubj.blocklistEnableDisableSwitch)); - - expect(formProps.onChange).toHaveBeenCalledWith({ - isValid: true, - updatedPolicy: expectedUpdatedPolicy, - }); - }); - - it('should allow blocklist to be enabled', () => { - set(formProps.policy, 'windows.malware.blocklist', false); - set(formProps.policy, 'mac.malware.blocklist', false); - set(formProps.policy, 'linux.malware.blocklist', false); - const expectedUpdatedPolicy = cloneDeep(formProps.policy); - set(expectedUpdatedPolicy, 'windows.malware.blocklist', true); - set(expectedUpdatedPolicy, 'mac.malware.blocklist', true); - set(expectedUpdatedPolicy, 'linux.malware.blocklist', true); - render(); - userEvent.click(renderResult.getByTestId(testSubj.blocklistEnableDisableSwitch)); - - expect(formProps.onChange).toHaveBeenCalledWith({ - isValid: true, - updatedPolicy: expectedUpdatedPolicy, - }); - }); + describe.each` + name | config | default + ${'blocklist'} | ${'blocklist'} | ${true} + ${'onWriteScan'} | ${'on_write_scan'} | ${true} + `( + '$name subfeature', + (feature: { name: 'blocklist' | 'onWriteScan'; config: string; deafult: boolean }) => { + it(`should set ${feature.name} to disabled if malware is turned off`, () => { + const expectedUpdatedPolicy = cloneDeep(formProps.policy); + setMalwareMode(expectedUpdatedPolicy, true); + render(); + userEvent.click(renderResult.getByTestId(testSubj.enableDisableSwitch)); + + expect(formProps.onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: expectedUpdatedPolicy, + }); + }); + + it(`should set ${feature.name} to enabled if malware is turned on`, () => { + const expectedUpdatedPolicy = cloneDeep(formProps.policy); + setMalwareMode(expectedUpdatedPolicy); + const initialPolicy = cloneDeep(formProps.policy); + setMalwareMode(initialPolicy, true); + render(initialPolicy); + + userEvent.click(renderResult.getByTestId(testSubj.enableDisableSwitch)); + + expect(formProps.onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: expectedUpdatedPolicy, + }); + }); + + it(`should allow ${feature.name} to be disabled`, () => { + const expectedUpdatedPolicy = cloneDeep(formProps.policy); + set(expectedUpdatedPolicy, `windows.malware.${feature.config}`, false); + set(expectedUpdatedPolicy, `mac.malware.${feature.config}`, false); + set(expectedUpdatedPolicy, `linux.malware.${feature.config}`, false); + render(); + userEvent.click(renderResult.getByTestId(testSubj[`${feature.name}EnableDisableSwitch`])); + + expect(formProps.onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: expectedUpdatedPolicy, + }); + }); + + it(`should allow ${feature.name} to be enabled`, () => { + set(formProps.policy, `windows.malware.${feature.config}`, false); + set(formProps.policy, `mac.malware.${feature.config}`, false); + set(formProps.policy, `linux.malware.${feature.config}`, false); + const expectedUpdatedPolicy = cloneDeep(formProps.policy); + set(expectedUpdatedPolicy, `windows.malware.${feature.config}`, true); + set(expectedUpdatedPolicy, `mac.malware.${feature.config}`, true); + set(expectedUpdatedPolicy, `linux.malware.${feature.config}`, true); + render(); + userEvent.click(renderResult.getByTestId(testSubj[`${feature.name}EnableDisableSwitch`])); + + expect(formProps.onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: expectedUpdatedPolicy, + }); + }); + } + ); describe('and displayed in View mode', () => { beforeEach(() => { @@ -124,6 +153,8 @@ describe('Policy Malware Protections Card', () => { 'Prevent' + 'Blocklist enabled' + 'Info' + + 'Scan files upon modification' + + 'Info' + 'User notification' + 'Agent version 7.11+' + 'Notify user' + @@ -168,6 +199,8 @@ describe('Policy Malware Protections Card', () => { 'Prevent' + 'Blocklist enabled' + 'Info' + + 'Scan files upon modification' + + 'Info' + 'User notification' + 'Agent version 7.11+' + 'Notify user' 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 index 0e5bec1d110c2..3b0f01e1b8d2d 100644 --- 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 @@ -7,10 +7,10 @@ import React, { memo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { EuiSpacer, EuiSwitch, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; import { OperatingSystem } from '@kbn/securitysolution-utils'; import { cloneDeep } from 'lodash'; +import { useIsExperimentalFeatureEnabled } from '../../../../../../../common/hooks/use_experimental_features'; import { useGetProtectionsUnavailableComponent } from '../../hooks/use_get_protections_unavailable_component'; import { NotifyUserOption } from '../notify_user_option'; import { SettingCard } from '../setting_card'; @@ -26,29 +26,83 @@ 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', - { +const BLOCKLIST_LABELS = { + enabled: i18n.translate('xpack.securitySolution.endpoint.policy.protections.blocklistEnabled', { defaultMessage: 'Blocklist enabled', + }), + disabled: i18n.translate('xpack.securitySolution.endpoint.policy.protections.blocklistDisabled', { + defaultMessage: 'Blocklist disabled', + }), + hint: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.blocklistTooltip', { + defaultMessage: + 'Enables or disables the blocklist associated with this policy. The blocklist is a collection hashes, paths, or signers which extends the list of processes the endpoint considers malicious. See the blocklist tab for entry details.', + }), +}; + +const ON_WRITE_SCAN_LABELS = { + enabled: i18n.translate('xpack.securitySolution.endpoint.policy.protections.onWriteScanEnabled', { + defaultMessage: 'Scan files upon modification', + }), + disabled: i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.onWriteScanDisabled', + { + defaultMessage: 'Files are not scanned upon modification', + } + ), + hint: i18n.translate('xpack.securitySolution.endpoint.policyDetailsConfig.onWriteScanTooltip', { + defaultMessage: + "Enables or disables scanning files when they're modified. Disabling this feature improves Endpoint performance.", + }), + versionCompatibilityHint: i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.onWriteVersionCompatibilityHint', + { + defaultMessage: 'Always enabled on Agent versions 8.13 and older.', + } + ), +}; + +type AdjustSubfeatureOnProtectionSwitch = NonNullable< + ProtectionSettingCardSwitchProps['additionalOnSwitchChange'] +>; + +// NOTE: it mutates `policyConfigData` passed on input +const adjustBlocklistSettingsOnProtectionSwitch: AdjustSubfeatureOnProtectionSwitch = ({ + value, + policyConfigData, + protectionOsList, +}) => { + for (const os of protectionOsList) { + policyConfigData[os].malware.blocklist = value; } -); -const BLOCKLIST_DISABLED_LABEL = i18n.translate( - 'xpack.securitySolution.endpoint.policy.protections.blocklistDisabled', - { - defaultMessage: 'Blocklist disabled', + return policyConfigData; +}; + +const adjustOnWriteSettingsOnProtectionSwitch: AdjustSubfeatureOnProtectionSwitch = ({ + value, + policyConfigData, + protectionOsList, +}) => { + for (const os of protectionOsList) { + policyConfigData[os].malware.on_write_scan = value; } -); -// 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; +}; - return policyConfigData; - }; +const adjustAllSubfeaturesOnProtectionSwitch: AdjustSubfeatureOnProtectionSwitch = ({ + policyConfigData, + ...rest +}) => { + const modifiedPolicy = adjustBlocklistSettingsOnProtectionSwitch({ + policyConfigData, + ...rest, + }); + return adjustOnWriteSettingsOnProtectionSwitch({ + policyConfigData: modifiedPolicy, + ...rest, + }); +}; const MALWARE_OS_VALUES: Immutable = [ PolicyOperatingSystem.windows, @@ -64,6 +118,9 @@ export type MalwareProtectionsProps = PolicyFormComponentCommonProps; */ export const MalwareProtectionsCard = React.memo( ({ policy, onChange, mode = 'edit', 'data-test-subj': dataTestSubj }) => { + const isMalwareOnwriteScanOptionAvailable = useIsExperimentalFeatureEnabled( + 'malwareOnWriteScanOptionAvailable' + ); const getTestId = useTestIdGenerator(dataTestSubj); const isProtectionsAllowed = !useGetProtectionsUnavailableComponent(); const protection = 'malware'; @@ -95,7 +152,7 @@ export const MalwareProtectionsCard = React.memo( protection={protection} protectionLabel={protectionLabel} osList={MALWARE_OS_VALUES} - additionalOnSwitchChange={adjustBlocklistSettingsOnProtectionSwitch} + additionalOnSwitchChange={adjustAllSubfeaturesOnProtectionSwitch} policy={policy} onChange={onChange} mode={mode} @@ -113,13 +170,31 @@ export const MalwareProtectionsCard = React.memo( /> - + {isMalwareOnwriteScanOptionAvailable && ( + <> + + + + )} + ( MalwareProtectionsCard.displayName = 'MalwareProtectionsCard'; -type EnableDisableBlocklistProps = PolicyFormComponentCommonProps; +type SubfeatureSwitchProps = PolicyFormComponentCommonProps & { + labels: { enabled: string; disabled: string; versionCompatibilityHint?: string; hint: string }; + adjustSubfeatureOnProtectionSwitch: AdjustSubfeatureOnProtectionSwitch; + checked: boolean; +}; -const EnableDisableBlocklist = memo( - ({ policy, onChange, mode, 'data-test-subj': dataTestSubj }) => { +const SubfeatureSwitch = memo( + ({ + policy, + onChange, + mode, + 'data-test-subj': dataTestSubj, + labels, + adjustSubfeatureOnProtectionSwitch, + checked, + }) => { const getTestId = useTestIdGenerator(dataTestSubj); - 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 label = checked ? labels.enabled : labels.disabled; const handleBlocklistSwitchChange = useCallback( (event) => { const value = event.target.checked; const newPayload = cloneDeep(policy); - adjustBlocklistSettingsOnProtectionSwitch({ + adjustSubfeatureOnProtectionSwitch({ value, policyConfigData: newPayload, protectionOsList: MALWARE_OS_VALUES, @@ -159,7 +246,7 @@ const EnableDisableBlocklist = memo( onChange({ isValid: true, updatedPolicy: newPayload }); }, - [onChange, policy] + [adjustSubfeatureOnProtectionSwitch, onChange, policy] ); return ( @@ -174,7 +261,7 @@ const EnableDisableBlocklist = memo( data-test-subj={getTestId('enableDisableSwitch')} /> ) : ( - <>{label} + label )} @@ -182,10 +269,9 @@ const EnableDisableBlocklist = memo( position="right" content={ <> - +

{labels.hint}

+ {labels.versionCompatibilityHint && } + {labels.versionCompatibilityHint && {labels.versionCompatibilityHint}} } /> @@ -194,4 +280,4 @@ const EnableDisableBlocklist = memo( ); } ); -EnableDisableBlocklist.displayName = 'EnableDisableBlocklist'; +SubfeatureSwitch.displayName = 'SubfeatureSwitch'; 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 index ee2e77307cd1b..5e1bb397f3b88 100644 --- 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 @@ -61,6 +61,7 @@ export const getPolicySettingsFormTestSubjects = ( rulesCallout: malwareTestSubj('rulesCallout'), blocklistContainer: malwareTestSubj('blocklist'), blocklistEnableDisableSwitch: malwareTestSubj('blocklist-enableDisableSwitch'), + onWriteScanEnableDisableSwitch: malwareTestSubj('onWriteScan-enableDisableSwitch'), }, ransomware: { card: ransomwareTestSubj(), @@ -192,13 +193,13 @@ export const exactMatchText = (text: string): RegExp => { * @param policy * @param turnOff * @param includePopup - * @param includeBlocklist + * @param includeSubfeatures */ export const setMalwareMode = ( policy: PolicyConfig, turnOff: boolean = false, includePopup: boolean = true, - includeBlocklist: boolean = true + includeSubfeatures: boolean = true ) => { const mode = turnOff ? ProtectionModes.off : ProtectionModes.prevent; const enableValue = mode !== ProtectionModes.off; @@ -207,15 +208,19 @@ export const setMalwareMode = ( set(policy, 'mac.malware.mode', mode); set(policy, 'linux.malware.mode', mode); - if (includeBlocklist) { - set(policy, 'windows.malware.blocklist', enableValue); - set(policy, 'mac.malware.blocklist', enableValue); - set(policy, 'linux.malware.blocklist', enableValue); - } - if (includePopup) { set(policy, 'windows.popup.malware.enabled', enableValue); set(policy, 'mac.popup.malware.enabled', enableValue); set(policy, 'linux.popup.malware.enabled', enableValue); } + + if (includeSubfeatures) { + set(policy, 'windows.malware.blocklist', enableValue); + set(policy, 'mac.malware.blocklist', enableValue); + set(policy, 'linux.malware.blocklist', enableValue); + + set(policy, 'windows.malware.on_write_scan', enableValue); + set(policy, 'mac.malware.on_write_scan', enableValue); + set(policy, 'linux.malware.on_write_scan', enableValue); + } }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.test.tsx index b7c4c13237a1f..bfc0ec3691363 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_layout/policy_settings_layout.test.tsx @@ -16,7 +16,11 @@ import { getUserPrivilegesMockDefaultValue } from '../../../../../common/compone import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks'; import { allFleetHttpMocks } from '../../../../mocks'; import userEvent from '@testing-library/user-event'; -import { expectIsViewOnly, getPolicySettingsFormTestSubjects } from '../policy_settings_form/mocks'; +import { + expectIsViewOnly, + getPolicySettingsFormTestSubjects, + setMalwareMode, +} from '../policy_settings_form/mocks'; import { cloneDeep, set } from 'lodash'; import { ProtectionModes } from '../../../../../../common/endpoint/types'; import { waitFor, cleanup } from '@testing-library/react'; @@ -89,15 +93,7 @@ describe('When rendering PolicySettingsLayout', () => { // Turn off malware userEvent.click(getByTestId(testSubj.malware.enableDisableSwitch)); - set(policySettings, 'windows.malware.mode', ProtectionModes.off); - set(policySettings, 'mac.malware.mode', ProtectionModes.off); - set(policySettings, 'linux.malware.mode', ProtectionModes.off); - set(policySettings, 'windows.malware.blocklist', false); - set(policySettings, 'mac.malware.blocklist', false); - set(policySettings, 'linux.malware.blocklist', false); - set(policySettings, 'windows.popup.malware.enabled', false); - set(policySettings, 'mac.popup.malware.enabled', false); - set(policySettings, 'linux.popup.malware.enabled', false); + setMalwareMode(policySettings, true); // Turn off Behaviour Protection userEvent.click(getByTestId(testSubj.behaviour.enableDisableSwitch));