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 890def5b63d4a..22037c021701f 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 @@ -68,3 +68,8 @@ export const factory = (): PolicyConfig => { }, }; }; + +/** + * Reflects what string the Endpoint will use when message field is default/empty + */ +export const DefaultMalwareMessage = 'Elastic Security { action } { filename }'; diff --git a/x-pack/plugins/security_solution/common/license/license.ts b/x-pack/plugins/security_solution/common/license/license.ts index 96c1a14ceb1f4..2d424ab9c960a 100644 --- a/x-pack/plugins/security_solution/common/license/license.ts +++ b/x-pack/plugins/security_solution/common/license/license.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { Observable, Subscription } from 'rxjs'; -import { ILicense } from '../../../licensing/common/types'; +import { ILicense, LicenseType } from '../../../licensing/common/types'; // Generic license service class that works with the license observable // Both server and client plugins instancates a singleton version of this class @@ -36,25 +36,20 @@ export class LicenseService { return this.observable; } - public isGoldPlus() { - return ( - this.licenseInformation?.isAvailable && - this.licenseInformation?.isActive && - this.licenseInformation?.hasAtLeast('gold') - ); + public isAtLeast(level: LicenseType): boolean { + return isAtLeast(this.licenseInformation, level); } - public isPlatinumPlus() { - return ( - this.licenseInformation?.isAvailable && - this.licenseInformation?.isActive && - this.licenseInformation?.hasAtLeast('platinum') - ); + public isGoldPlus(): boolean { + return this.isAtLeast('gold'); } - public isEnterprise() { - return ( - this.licenseInformation?.isAvailable && - this.licenseInformation?.isActive && - this.licenseInformation?.hasAtLeast('enterprise') - ); + public isPlatinumPlus(): boolean { + return this.isAtLeast('platinum'); + } + public isEnterprise(): boolean { + return this.isAtLeast('enterprise'); } } + +export const isAtLeast = (license: ILicense | null, level: LicenseType): boolean => { + return license !== null && license.isAvailable && license.isActive && license.hasAtLeast(level); +}; 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 new file mode 100644 index 0000000000000..6923bf00055f6 --- /dev/null +++ b/x-pack/plugins/security_solution/common/license/policy_config.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + isEndpointPolicyValidForLicense, + unsetPolicyFeaturesAboveLicenseLevel, +} from './policy_config'; +import { DefaultMalwareMessage, factory } from '../endpoint/models/policy_config'; +import { licenseMock } from '../../../licensing/common/licensing.mock'; + +describe('policy_config and licenses', () => { + const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); + const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } }); + const Basic = licenseMock.createLicense({ license: { type: 'basic', mode: 'basic' } }); + + describe('isEndpointPolicyValidForLicense', () => { + it('allows malware notification to be disabled with a Platinum license', () => { + const policy = factory(); + policy.windows.popup.malware.enabled = false; // make policy change + const valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeTruthy(); + }); + it('blocks windows malware notification changes below Platinum licenses', () => { + const policy = factory(); + policy.windows.popup.malware.enabled = false; // make policy change + let valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + + it('blocks mac malware notification changes below Platinum licenses', () => { + const policy = factory(); + policy.mac.popup.malware.enabled = false; // make policy change + let valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + + it('allows malware notification message changes with a Platinum license', () => { + const policy = factory(); + policy.windows.popup.malware.message = 'BOOM'; // make policy change + const valid = isEndpointPolicyValidForLicense(policy, Platinum); + expect(valid).toBeTruthy(); + }); + it('blocks windows malware notification message changes below Platinum licenses', () => { + const policy = factory(); + policy.windows.popup.malware.message = 'BOOM'; // make policy change + let valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + it('blocks mac malware notification message changes below Platinum licenses', () => { + const policy = factory(); + policy.mac.popup.malware.message = 'BOOM'; // make policy change + let valid = isEndpointPolicyValidForLicense(policy, Gold); + expect(valid).toBeFalsy(); + + valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeFalsy(); + }); + + it('allows default policyConfig with Basic', () => { + const policy = factory(); + const valid = isEndpointPolicyValidForLicense(policy, Basic); + expect(valid).toBeTruthy(); + }); + }); + + describe('unsetPolicyFeaturesAboveLicenseLevel', () => { + it('does not change any fields with a Platinum license', () => { + const policy = factory(); + const popupMessage = 'WOOP WOOP'; + policy.windows.popup.malware.message = popupMessage; + policy.mac.popup.malware.message = popupMessage; + policy.windows.popup.malware.enabled = false; + + const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(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); + }); + it('resets Platinum-paid fields for lower license tiers', () => { + const defaults = factory(); // reference + const policy = factory(); // what we will modify, and should be reset + const popupMessage = 'WOOP WOOP'; + policy.windows.popup.malware.message = popupMessage; + policy.mac.popup.malware.message = popupMessage; + policy.windows.popup.malware.enabled = false; + + const retPolicy = unsetPolicyFeaturesAboveLicenseLevel(policy, Gold); + expect(retPolicy.windows.popup.malware.enabled).toEqual( + defaults.windows.popup.malware.enabled + ); + expect(retPolicy.windows.popup.malware.message).not.toEqual(popupMessage); + expect(retPolicy.mac.popup.malware.message).not.toEqual(popupMessage); + + // need to invert the test, since it could be either value + expect(['', DefaultMalwareMessage]).toContain(retPolicy.windows.popup.malware.message); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/license/policy_config.ts b/x-pack/plugins/security_solution/common/license/policy_config.ts new file mode 100644 index 0000000000000..da2260ad55e8b --- /dev/null +++ b/x-pack/plugins/security_solution/common/license/policy_config.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ILicense } from '../../../licensing/common/types'; +import { isAtLeast } from './license'; +import { PolicyConfig } from '../endpoint/types'; +import { DefaultMalwareMessage, factory } from '../endpoint/models/policy_config'; + +/** + * Given an endpoint package policy, verifies that all enabled features that + * require a certain license level have a valid license for them. + */ +export const isEndpointPolicyValidForLicense = ( + policy: PolicyConfig, + license: ILicense | null +): boolean => { + if (isAtLeast(license, 'platinum')) { + return true; // currently, platinum allows all features + } + + const defaults = factory(); + + // only platinum or higher may disable malware notification + if ( + policy.windows.popup.malware.enabled !== defaults.windows.popup.malware.enabled || + policy.mac.popup.malware.enabled !== defaults.mac.popup.malware.enabled + ) { + return false; + } + + // Only Platinum or higher may change the malware message (which can be blank or what Endpoint defaults) + if ( + [policy.windows, policy.mac].some( + (p) => p.popup.malware.message !== '' && p.popup.malware.message !== DefaultMalwareMessage + ) + ) { + return false; + } + + return true; +}; + +/** + * Resets paid features in a PolicyConfig back to default values + * when unsupported by the given license level. + */ +export const unsetPolicyFeaturesAboveLicenseLevel = ( + policy: PolicyConfig, + license: ILicense | null +): PolicyConfig => { + if (isAtLeast(license, 'platinum')) { + return policy; + } + + const defaults = factory(); + // set any license-gated features back to the defaults + policy.windows.popup.malware.enabled = defaults.windows.popup.malware.enabled; + policy.mac.popup.malware.enabled = defaults.mac.popup.malware.enabled; + policy.windows.popup.malware.message = defaults.windows.popup.malware.message; + policy.mac.popup.malware.message = defaults.mac.popup.malware.message; + + return policy; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts index 36649d22f730c..f039324b3af64 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts @@ -5,6 +5,7 @@ */ import { IHttpFetchError } from 'kibana/public'; +import { DefaultMalwareMessage } from '../../../../../../common/endpoint/models/policy_config'; import { PolicyDetailsState, UpdatePolicyResponse } from '../../types'; import { policyIdFromParams, @@ -38,10 +39,8 @@ export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory