diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index e2e33f1ee6c26..4331baef11001 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -42,6 +42,7 @@ import { migrateSettingsToV7130, migrateOutputToV7130, } from './migrations/to_v7_13_0'; +import { migratePackagePolicyToV7140 } from './migrations/to_v7_14_0'; /* * Saved object types and mappings @@ -267,6 +268,7 @@ const getSavedObjectTypes = ( '7.11.0': migratePackagePolicyToV7110, '7.12.0': migratePackagePolicyToV7120, '7.13.0': migratePackagePolicyToV7130, + '7.14.0': migratePackagePolicyToV7140, }, }, [PACKAGES_SAVED_OBJECT_TYPE]: { 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 ddce95a96879a..b4f09e541298a 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 @@ -8,3 +8,4 @@ export { migratePackagePolicyToV7110 } from './to_v7_11_0'; export { migratePackagePolicyToV7120 } from './to_v7_12_0'; export { migrateEndpointPackagePolicyToV7130 } from './to_v7_13_0'; +export { migrateEndpointPackagePolicyToV7140 } from './to_v7_14_0'; diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_14_0.test.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_14_0.test.ts new file mode 100644 index 0000000000000..8ccb50735d4fb --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_14_0.test.ts @@ -0,0 +1,200 @@ +/* + * 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 { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from 'kibana/server'; + +import type { PackagePolicy } from '../../../../common'; + +import { migrateEndpointPackagePolicyToV7140 } from './to_v7_14_0'; + +describe('7.14.0 Endpoint Package Policy migration', () => { + const migration = migrateEndpointPackagePolicyToV7140; + it('adds supported option for ransomware on migrations', () => { + const doc = { + id: 'mock-saved-object-id', + attributes: { + name: 'Some Policy Name', + package: { + name: 'endpoint', + title: '', + version: '', + }, + id: 'endpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: { + windows: { + ransomware: { + mode: 'off', + }, + malware: { + mode: 'off', + }, + popup: { + malware: { + message: '', + enabled: false, + }, + ransomware: { + message: '', + enabled: false, + }, + }, + }, + linux: { + events: { process: true, file: true, network: true }, + logging: { file: 'info' }, + }, + }, + }, + }, + }, + ], + }, + type: ' nested', + }; + + expect(migration(doc, {} as SavedObjectMigrationContext)).toEqual({ + attributes: { + name: 'Some Policy Name', + package: { + name: 'endpoint', + title: '', + version: '', + }, + id: 'endpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: { + windows: { + ransomware: { + mode: 'off', + supported: true, + }, + malware: { + mode: 'off', + }, + popup: { + malware: { + message: '', + enabled: false, + }, + ransomware: { + message: '', + enabled: false, + }, + }, + }, + linux: { + events: { process: true, file: true, network: true }, + logging: { file: 'info' }, + }, + }, + }, + }, + }, + ], + }, + type: ' nested', + id: 'mock-saved-object-id', + }); + }); + + it('does not modify non-endpoint package policies', () => { + const doc: SavedObjectUnsanitizedDoc = { + id: 'mock-saved-object-id', + attributes: { + name: 'Some Policy Name', + package: { + name: 'notEndpoint', + title: '', + version: '', + }, + id: 'notEndpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'notEndpoint', + enabled: true, + streams: [], + config: {}, + }, + ], + }, + type: ' nested', + }; + + expect( + migration(doc, {} as SavedObjectMigrationContext) as SavedObjectUnsanitizedDoc + ).toEqual({ + attributes: { + name: 'Some Policy Name', + package: { + name: 'notEndpoint', + title: '', + version: '', + }, + id: 'notEndpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'notEndpoint', + enabled: true, + streams: [], + config: {}, + }, + ], + }, + type: ' nested', + id: 'mock-saved-object-id', + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_14_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_14_0.ts new file mode 100644 index 0000000000000..a9acf7865f812 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_14_0.ts @@ -0,0 +1,33 @@ +/* + * 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 { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { cloneDeep } from 'lodash'; + +import type { PackagePolicy } from '../../../../common'; + +export const migrateEndpointPackagePolicyToV7140: SavedObjectMigrationFn< + PackagePolicy, + PackagePolicy +> = (packagePolicyDoc) => { + const updatedPackagePolicyDoc: SavedObjectUnsanitizedDoc = cloneDeep( + packagePolicyDoc + ); + + if (packagePolicyDoc.attributes.package?.name === 'endpoint') { + const input = updatedPackagePolicyDoc.attributes.inputs[0]; + if (input && input.config) { + const policy = input.config.policy.value; + + // This value is based on license. + // For the migration, we add 'true', our license watcher will correct it, if needed, when the app starts. + policy.windows.ransomware.supported = true; + } + } + + return updatedPackagePolicyDoc; +}; diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_14_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_14_0.ts new file mode 100644 index 0000000000000..3255e15c6ceec --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_14_0.ts @@ -0,0 +1,29 @@ +/* + * 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 { SavedObjectMigrationFn } from 'kibana/server'; + +import type { PackagePolicy } from '../../../common'; + +import { migrateEndpointPackagePolicyToV7140 } from './security_solution'; + +export const migratePackagePolicyToV7140: SavedObjectMigrationFn = ( + packagePolicyDoc, + migrationContext +) => { + let updatedPackagePolicyDoc = packagePolicyDoc; + + // Endpoint specific migrations + if (packagePolicyDoc.attributes.package?.name === 'endpoint') { + updatedPackagePolicyDoc = migrateEndpointPackagePolicyToV7140( + packagePolicyDoc, + migrationContext + ); + } + + return updatedPackagePolicyDoc; +}; 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 cbac2d03cfb97..1bb5cf7bc87c1 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 @@ -27,6 +27,7 @@ export const policyFactory = (): PolicyConfig => { }, ransomware: { mode: ProtectionModes.prevent, + supported: true, }, popup: { malware: { @@ -89,6 +90,7 @@ export const policyFactoryWithoutPaidFeatures = ( ...policy.windows, ransomware: { mode: ProtectionModes.off, + supported: false, }, popup: { ...policy.windows.popup, @@ -115,6 +117,24 @@ export const policyFactoryWithoutPaidFeatures = ( }; }; +/** + * Strips paid features from an existing or new `PolicyConfig` for gold and below license + */ +export const policyFactoryWithSupportedFeatures = ( + policy: PolicyConfig = policyFactory() +): PolicyConfig => { + return { + ...policy, + windows: { + ...policy.windows, + ransomware: { + ...policy.windows.ransomware, + supported: true, + }, + }, + }; +}; + /** * Reflects what string the Endpoint will use when message field is default/empty */ 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 b9e72bcd625ec..541507ac048ca 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -835,7 +835,7 @@ export interface PolicyConfig { security: boolean; }; malware: ProtectionFields; - ransomware: ProtectionFields; + ransomware: ProtectionFields & SupportedFields; logging: { file: string; }; @@ -910,6 +910,11 @@ export interface ProtectionFields { mode: ProtectionModes; } +/** Policy: Supported fields */ +export interface SupportedFields { + supported: boolean; +} + /** Policy protection mode options */ export enum ProtectionModes { detect = 'detect', diff --git a/x-pack/plugins/security_solution/common/license/policy_config.test.ts b/x-pack/plugins/security_solution/common/license/policy_config.test.ts index e8637e43ce1c7..219538184765a 100644 --- a/x-pack/plugins/security_solution/common/license/policy_config.test.ts +++ b/x-pack/plugins/security_solution/common/license/policy_config.test.ts @@ -7,11 +7,12 @@ import { isEndpointPolicyValidForLicense, - unsetPolicyFeaturesAboveLicenseLevel, + unsetPolicyFeaturesAccordingToLicenseLevel, } from './policy_config'; import { DefaultMalwareMessage, policyFactory, + policyFactoryWithSupportedFeatures, policyFactoryWithoutPaidFeatures, } from '../endpoint/models/policy_config'; import { licenseMock } from '../../../licensing/common/licensing.mock'; @@ -77,6 +78,7 @@ describe('policy_config and licenses', () => { it('allows ransomware to be turned on for Platinum licenses', () => { const policy = policyFactoryWithoutPaidFeatures(); policy.windows.ransomware.mode = ProtectionModes.prevent; + policy.windows.ransomware.supported = true; const valid = isEndpointPolicyValidForLicense(policy, Platinum); expect(valid).toBeTruthy(); @@ -94,6 +96,7 @@ describe('policy_config and licenses', () => { it('allows ransomware notification to be turned on with a Platinum license', () => { const policy = policyFactoryWithoutPaidFeatures(); policy.windows.popup.ransomware.enabled = true; + policy.windows.ransomware.supported = true; const valid = isEndpointPolicyValidForLicense(policy, Platinum); expect(valid).toBeTruthy(); }); @@ -130,7 +133,7 @@ describe('policy_config and licenses', () => { }); }); - describe('unsetPolicyFeaturesAboveLicenseLevel', () => { + describe('unsetPolicyFeaturesAccordingToLicenseLevel', () => { it('does not change any malware fields with a Platinum license', () => { const policy = policyFactory(); const popupMessage = 'WOOP WOOP'; @@ -138,7 +141,7 @@ describe('policy_config and licenses', () => { policy.mac.popup.malware.message = popupMessage; policy.windows.popup.malware.enabled = false; - const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(policy, Platinum); + const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Platinum); expect(retPolicy.windows.popup.malware.enabled).toBeFalsy(); expect(retPolicy.windows.popup.malware.message).toEqual(popupMessage); expect(retPolicy.mac.popup.malware.message).toEqual(popupMessage); @@ -151,7 +154,7 @@ describe('policy_config and licenses', () => { policy.windows.popup.ransomware.enabled = false; policy.windows.popup.ransomware.message = popupMessage; - const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(policy, Platinum); + const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Platinum); expect(retPolicy.windows.ransomware.mode).toEqual(ProtectionModes.detect); expect(retPolicy.windows.popup.ransomware.enabled).toBeFalsy(); expect(retPolicy.windows.popup.ransomware.message).toEqual(popupMessage); @@ -167,7 +170,7 @@ describe('policy_config and licenses', () => { policy.windows.popup.ransomware.message = popupMessage; policy.windows.popup.ransomware.enabled = false; - const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(policy, Gold); + const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Gold); expect(retPolicy.windows.popup.malware.enabled).toEqual( defaults.windows.popup.malware.enabled ); @@ -183,7 +186,7 @@ describe('policy_config and licenses', () => { const popupMessage = 'WOOP WOOP'; policy.windows.popup.ransomware.message = popupMessage; - const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(policy, Gold); + const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Gold); expect(retPolicy.windows.ransomware.mode).toEqual(defaults.windows.ransomware.mode); expect(retPolicy.windows.popup.ransomware.enabled).toEqual( @@ -194,6 +197,26 @@ describe('policy_config and licenses', () => { // need to invert the test, since it could be either value expect(['', DefaultMalwareMessage]).toContain(retPolicy.windows.popup.ransomware.message); }); + + it('sets ransomware supported field to false when license is below Platinum', () => { + const defaults = policyFactoryWithoutPaidFeatures(); // reference + const policy = policyFactory(); // what we will modify, and should be reset + policy.windows.ransomware.supported = true; + + const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Gold); + + expect(retPolicy.windows.ransomware.supported).toEqual(defaults.windows.ransomware.supported); + }); + + it('sets ransomware supported field to true when license is at Platinum', () => { + const defaults = policyFactoryWithSupportedFeatures(); // reference + const policy = policyFactory(); // what we will modify, and should be reset + policy.windows.ransomware.supported = false; + + const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Platinum); + + expect(retPolicy.windows.ransomware.supported).toEqual(defaults.windows.ransomware.supported); + }); }); describe('policyFactoryWithoutPaidFeatures for gold and below license', () => { diff --git a/x-pack/plugins/security_solution/common/license/policy_config.ts b/x-pack/plugins/security_solution/common/license/policy_config.ts index 903e241b1b490..171f2d9d0287d 100644 --- a/x-pack/plugins/security_solution/common/license/policy_config.ts +++ b/x-pack/plugins/security_solution/common/license/policy_config.ts @@ -11,6 +11,7 @@ import { PolicyConfig } from '../endpoint/types'; import { DefaultMalwareMessage, policyFactoryWithoutPaidFeatures, + policyFactoryWithSupportedFeatures, } from '../endpoint/models/policy_config'; /** @@ -22,6 +23,13 @@ export const isEndpointPolicyValidForLicense = ( license: ILicense | null ): boolean => { if (isAtLeast(license, 'platinum')) { + const defaults = policyFactoryWithSupportedFeatures(); + + // only platinum or higher may enable ransomware + if (policy.windows.ransomware.supported !== defaults.windows.ransomware.supported) { + return false; + } + return true; // currently, platinum allows all features } @@ -62,6 +70,11 @@ export const isEndpointPolicyValidForLicense = ( return false; } + // only platinum or higher may enable ransomware + if (policy.windows.ransomware.supported !== defaults.windows.ransomware.supported) { + return false; + } + return true; }; @@ -69,12 +82,12 @@ export const isEndpointPolicyValidForLicense = ( * Resets paid features in a PolicyConfig back to default values * when unsupported by the given license level. */ -export const unsetPolicyFeaturesAboveLicenseLevel = ( +export const unsetPolicyFeaturesAccordingToLicenseLevel = ( policy: PolicyConfig, license: ILicense | null ): PolicyConfig => { if (isAtLeast(license, 'platinum')) { - return policy; + return policyFactoryWithSupportedFeatures(policy); } // set any license-gated features back to the defaults 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 74dfbe4dec3ba..89b8e9d7f5944 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 @@ -284,7 +284,7 @@ describe('policy details: ', () => { security: true, }, malware: { mode: 'prevent' }, - ransomware: { mode: 'off' }, + ransomware: { mode: 'off', supported: false }, popup: { malware: { enabled: true, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts index 98e1fd1f29d97..078aea4ec41ac 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts @@ -8,7 +8,7 @@ import { createSelector } from 'reselect'; import { matchPath } from 'react-router-dom'; import { ILicense } from '../../../../../../../licensing/common/types'; -import { unsetPolicyFeaturesAboveLicenseLevel } from '../../../../../../common/license/policy_config'; +import { unsetPolicyFeaturesAccordingToLicenseLevel } from '../../../../../../common/license/policy_config'; import { PolicyDetailsState } from '../../types'; import { Immutable, @@ -33,7 +33,7 @@ export const licensedPolicy: ( licenseState, (policyData, license) => { if (policyData) { - const policyValue = unsetPolicyFeaturesAboveLicenseLevel( + const policyValue = unsetPolicyFeaturesAccordingToLicenseLevel( policyData.inputs[0].config.policy.value, license as ILicense ); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts index 58a73d31708a1..b8c6e57f72cea 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts @@ -24,9 +24,9 @@ import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; import { ILicense } from '../../../../../licensing/common/types'; import { isEndpointPolicyValidForLicense, - unsetPolicyFeaturesAboveLicenseLevel, + unsetPolicyFeaturesAccordingToLicenseLevel, } from '../../../../common/license/policy_config'; -import { isAtLeast, LicenseService } from '../../../../common/license/license'; +import { LicenseService } from '../../../../common/license/license'; export class PolicyWatcher { private logger: Logger; @@ -76,10 +76,6 @@ export class PolicyWatcher { } public async watch(license: ILicense) { - if (isAtLeast(license, 'platinum')) { - return; - } - let page = 1; let response: { items: PackagePolicy[]; @@ -114,7 +110,7 @@ export class PolicyWatcher { }; const policyConfig = updatePolicy.inputs[0].config?.policy.value; if (!isEndpointPolicyValidForLicense(policyConfig, license)) { - updatePolicy.inputs[0].config!.policy.value = unsetPolicyFeaturesAboveLicenseLevel( + updatePolicy.inputs[0].config!.policy.value = unsetPolicyFeaturesAccordingToLicenseLevel( policyConfig, license ); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index fc814d7d2b060..b2e75297570af 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -285,7 +285,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, logging: { file: 'info' }, malware: { mode: 'prevent' }, - ransomware: { mode: 'prevent' }, + ransomware: { mode: 'prevent', supported: true }, popup: { malware: { enabled: true, @@ -446,7 +446,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, logging: { file: 'info' }, malware: { mode: 'prevent' }, - ransomware: { mode: 'prevent' }, + ransomware: { mode: 'prevent', supported: true }, popup: { malware: { enabled: true, @@ -604,7 +604,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }, logging: { file: 'info' }, malware: { mode: 'prevent' }, - ransomware: { mode: 'prevent' }, + ransomware: { mode: 'prevent', supported: true }, popup: { malware: { enabled: true,