From 27c394c936991ce11c5cc1fdf7184c8fc4bdcd88 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Fri, 11 Aug 2023 09:32:25 -0400 Subject: [PATCH] [Security Solution][Endpoint] Add API checks to Endpoint Policy create/update for checking `endpointPolicyProtections` is enabled (#163429) ## Summary - Adds checks to both the Policy Create and Policy Update APIs (Fleet API extension points) and turns off all protections if `endpointPolicyProtections` appFeature is disabled - Adds migration of policies to the Plugin `start()` that will check if `endpointPolicyProtections` is disabled and updates all existing policies (if necessary) to disable protections. --- .../models/policy_config_helpers.test.ts | 93 +++++++++-- .../endpoint/models/policy_config_helpers.ts | 100 +++++++++++- .../common/endpoint/types/utility_types.ts | 12 ++ .../endpoint/endpoint_app_context_services.ts | 9 +- .../turn_off_policy_protections.test.ts | 145 ++++++++++++++++++ .../migrations/turn_off_policy_protections.ts | 107 +++++++++++++ .../server/endpoint/mocks.ts | 6 +- .../fleet/endpoint_fleet_services_factory.ts | 10 ++ .../create_internal_readonly_so_client.ts | 21 +-- .../utils/create_internal_so_client.ts | 27 ++++ .../fleet_integration.test.ts | 69 ++++++++- .../fleet_integration/fleet_integration.ts | 48 ++++-- .../handlers/create_default_policy.test.ts | 40 ++++- .../handlers/create_default_policy.ts | 26 +++- .../server/lib/app_features/app_features.ts | 2 +- .../server/lib/app_features/mocks.ts | 38 +++++ .../security_solution/server/plugin.ts | 33 ++-- 17 files changed, 715 insertions(+), 71 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/types/utility_types.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/create_internal_so_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/app_features/mocks.ts 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 1b01d477f1995e..fe3fd8c2ebd6a9 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 @@ -6,14 +6,19 @@ */ import type { PolicyConfig } from '../types'; -import { ProtectionModes } from '../types'; +import { PolicyOperatingSystem, ProtectionModes } from '../types'; import { policyFactory } from './policy_config'; -import { disableProtections } from './policy_config_helpers'; +import { + disableProtections, + isPolicySetToEventCollectionOnly, + ensureOnlyEventCollectionIsAllowed, +} from './policy_config_helpers'; +import { set } from 'lodash'; describe('Policy Config helpers', () => { describe('disableProtections', () => { it('disables all the protections in the default policy', () => { - expect(disableProtections(policyFactory())).toEqual(eventsOnlyPolicy); + expect(disableProtections(policyFactory())).toEqual(eventsOnlyPolicy()); }); it('does not enable supported fields', () => { @@ -51,20 +56,20 @@ describe('Policy Config helpers', () => { }; const expectedPolicyWithoutSupportedProtections: PolicyConfig = { - ...eventsOnlyPolicy, + ...eventsOnlyPolicy(), windows: { - ...eventsOnlyPolicy.windows, + ...eventsOnlyPolicy().windows, memory_protection: notSupported, behavior_protection: notSupportedBehaviorProtection, ransomware: notSupported, }, mac: { - ...eventsOnlyPolicy.mac, + ...eventsOnlyPolicy().mac, memory_protection: notSupported, behavior_protection: notSupportedBehaviorProtection, }, linux: { - ...eventsOnlyPolicy.linux, + ...eventsOnlyPolicy().linux, memory_protection: notSupported, behavior_protection: notSupportedBehaviorProtection, }, @@ -104,10 +109,10 @@ describe('Policy Config helpers', () => { }; const expectedPolicy: PolicyConfig = { - ...eventsOnlyPolicy, - windows: { ...eventsOnlyPolicy.windows, events: { ...windowsEvents } }, - mac: { ...eventsOnlyPolicy.mac, events: { ...macEvents } }, - linux: { ...eventsOnlyPolicy.linux, events: { ...linuxEvents } }, + ...eventsOnlyPolicy(), + windows: { ...eventsOnlyPolicy().windows, events: { ...windowsEvents } }, + mac: { ...eventsOnlyPolicy().mac, events: { ...macEvents } }, + linux: { ...eventsOnlyPolicy().linux, events: { ...linuxEvents } }, }; const inputPolicy = { @@ -120,11 +125,73 @@ describe('Policy Config helpers', () => { expect(disableProtections(inputPolicy)).toEqual(expectedPolicy); }); }); + + describe('setPolicyToEventCollectionOnly()', () => { + it('should set the policy to event collection only', () => { + expect(ensureOnlyEventCollectionIsAllowed(policyFactory())).toEqual(eventsOnlyPolicy()); + }); + }); + + describe('isPolicySetToEventCollectionOnly', () => { + let policy: PolicyConfig; + + beforeEach(() => { + policy = ensureOnlyEventCollectionIsAllowed(policyFactory()); + }); + + it.each([ + { + keyPath: `${PolicyOperatingSystem.windows}.malware.mode`, + keyValue: ProtectionModes.prevent, + expectedResult: false, + }, + { + keyPath: `${PolicyOperatingSystem.mac}.malware.mode`, + keyValue: ProtectionModes.off, + expectedResult: true, + }, + { + keyPath: `${PolicyOperatingSystem.windows}.ransomware.mode`, + keyValue: ProtectionModes.prevent, + expectedResult: false, + }, + { + keyPath: `${PolicyOperatingSystem.linux}.memory_protection.mode`, + keyValue: ProtectionModes.off, + expectedResult: true, + }, + { + keyPath: `${PolicyOperatingSystem.mac}.behavior_protection.mode`, + keyValue: ProtectionModes.detect, + expectedResult: false, + }, + { + keyPath: `${PolicyOperatingSystem.windows}.attack_surface_reduction.credential_hardening.enabled`, + keyValue: true, + expectedResult: false, + }, + { + keyPath: `${PolicyOperatingSystem.windows}.antivirus_registration.enabled`, + keyValue: true, + expectedResult: false, + }, + ])( + 'should return `$expectedResult` if `$keyPath` is set to `$keyValue`', + ({ keyPath, keyValue, expectedResult }) => { + set(policy, keyPath, keyValue); + + expect(isPolicySetToEventCollectionOnly(policy)).toEqual({ + isOnlyCollectingEvents: expectedResult, + message: expectedResult ? undefined : `property [${keyPath}] is set to [${keyValue}]`, + }); + } + ); + }); }); // This constant makes sure that if the type `PolicyConfig` is ever modified, // the logic for disabling protections is also modified due to type check. -export const eventsOnlyPolicy: PolicyConfig = { +export const eventsOnlyPolicy = (): PolicyConfig => ({ meta: { license: '', cloud: false, license_uid: '', cluster_name: '', cluster_uuid: '' }, windows: { events: { @@ -187,4 +254,4 @@ export const eventsOnlyPolicy: PolicyConfig = { capture_env_vars: 'LD_PRELOAD,LD_LIBRARY_PATH', }, }, -}; +}); 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 4bdca10547bc29..cb460e2f75f49d 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 @@ -5,8 +5,63 @@ * 2.0. */ +import { get, set } from 'lodash'; import type { PolicyConfig } from '../types'; -import { ProtectionModes } from '../types'; +import { PolicyOperatingSystem, ProtectionModes } from '../types'; + +interface PolicyProtectionReference { + keyPath: string; + osList: PolicyOperatingSystem[]; + enableValue: unknown; + disableValue: unknown; +} + +const getPolicyProtectionsReference = (): PolicyProtectionReference[] => { + const allOsValues = [ + PolicyOperatingSystem.mac, + PolicyOperatingSystem.linux, + PolicyOperatingSystem.windows, + ]; + + return [ + { + keyPath: 'malware.mode', + osList: [...allOsValues], + disableValue: ProtectionModes.off, + enableValue: ProtectionModes.prevent, + }, + { + keyPath: 'ransomware.mode', + osList: [PolicyOperatingSystem.windows], + disableValue: ProtectionModes.off, + enableValue: ProtectionModes.prevent, + }, + { + keyPath: 'memory_protection.mode', + osList: [...allOsValues], + disableValue: ProtectionModes.off, + enableValue: ProtectionModes.prevent, + }, + { + keyPath: 'behavior_protection.mode', + osList: [...allOsValues], + disableValue: ProtectionModes.off, + enableValue: ProtectionModes.prevent, + }, + { + keyPath: 'attack_surface_reduction.credential_hardening.enabled', + osList: [PolicyOperatingSystem.windows], + disableValue: false, + enableValue: true, + }, + { + keyPath: 'antivirus_registration.enabled', + osList: [PolicyOperatingSystem.windows], + disableValue: false, + enableValue: true, + }, + ]; +}; /** * Returns a copy of the passed `PolicyConfig` with all protections set to disabled. @@ -106,3 +161,46 @@ const getDisabledWindowsSpecificPopups = (policy: PolicyConfig) => ({ enabled: false, }, }); + +/** + * Returns the provided with only event collection turned enabled + * @param policy + */ +export const ensureOnlyEventCollectionIsAllowed = (policy: PolicyConfig): PolicyConfig => { + const updatedPolicy = disableProtections(policy); + + set(updatedPolicy, 'windows.antivirus_registration.enabled', false); + + return updatedPolicy; +}; + +/** + * Checks to see if the provided policy is set to Event Collection only + */ +export const isPolicySetToEventCollectionOnly = ( + policy: PolicyConfig +): { isOnlyCollectingEvents: boolean; message?: string } => { + const protectionsRef = getPolicyProtectionsReference(); + let message: string | undefined; + + const hasEnabledProtection = protectionsRef.some(({ keyPath, osList, disableValue }) => { + const hasOsPropertyEnabled = osList.some((osValue) => { + const fullKeyPathForOs = `${osValue}.${keyPath}`; + const currentValue = get(policy, fullKeyPathForOs); + const isEnabled = currentValue !== disableValue; + + if (isEnabled) { + message = `property [${fullKeyPathForOs}] is set to [${currentValue}]`; + } + + return isEnabled; + }); + + return hasOsPropertyEnabled; + }); + + return { + isOnlyCollectingEvents: !hasEnabledProtection, + message, + }; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/utility_types.ts b/x-pack/plugins/security_solution/common/endpoint/types/utility_types.ts new file mode 100644 index 00000000000000..92880d93221914 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types/utility_types.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export type PromiseResolvedValue> = T extends Promise + ? Value + : never; diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 50d4ae02eeb9bf..058f8892013a06 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -17,6 +17,7 @@ import type { import type { PluginStartContract as AlertsPluginStartContract } from '@kbn/alerting-plugin/server'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions/types'; +import type { AppFeatures } from '../lib/app_features'; import { getPackagePolicyCreateCallback, getPackagePolicyUpdateCallback, @@ -69,6 +70,7 @@ export interface EndpointAppContextServiceStartContract { actionCreateService: ActionCreateService | undefined; cloud: CloudSetup; esClient: ElasticsearchClient; + appFeatures: AppFeatures; } /** @@ -106,6 +108,7 @@ export class EndpointAppContextService { featureUsageService, endpointMetadataService, esClient, + appFeatures, } = dependencies; registerIngestCallback( @@ -117,7 +120,8 @@ export class EndpointAppContextService { alerting, licenseService, exceptionListsClient, - cloud + cloud, + appFeatures ) ); @@ -134,7 +138,8 @@ export class EndpointAppContextService { featureUsageService, endpointMetadataService, cloud, - esClient + esClient, + appFeatures ) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts new file mode 100644 index 00000000000000..1d39b72670b986 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { createMockEndpointAppContextServiceStartContract } from '../mocks'; +import type { Logger } from '@kbn/logging'; +import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { EndpointInternalFleetServicesInterface } from '../services/fleet'; +import type { AppFeatures } from '../../lib/app_features'; +import { createAppFeaturesMock } from '../../lib/app_features/mocks'; +import { ALL_APP_FEATURE_KEYS } from '../../../common'; +import { turnOffPolicyProtectionsIfNotSupported } from './turn_off_policy_protections'; +import { FleetPackagePolicyGenerator } from '../../../common/endpoint/data_generators/fleet_package_policy_generator'; +import type { PolicyData } from '../../../common/endpoint/types'; +import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; +import type { PromiseResolvedValue } from '../../../common/endpoint/types/utility_types'; +import { ensureOnlyEventCollectionIsAllowed } from '../../../common/endpoint/models/policy_config_helpers'; + +describe('Turn Off Policy Protections Migration', () => { + let esClient: ElasticsearchClient; + let fleetServices: EndpointInternalFleetServicesInterface; + let appFeatures: AppFeatures; + let logger: Logger; + + const callTurnOffPolicyProtections = () => + turnOffPolicyProtectionsIfNotSupported(esClient, fleetServices, appFeatures, logger); + + beforeEach(() => { + const endpointContextStartContract = createMockEndpointAppContextServiceStartContract(); + + ({ esClient, appFeatures, logger } = endpointContextStartContract); + fleetServices = endpointContextStartContract.endpointFleetServicesFactory.asInternalUser(); + }); + + describe('and `endpointPolicyProtections` is enabled', () => { + it('should do nothing', async () => { + await callTurnOffPolicyProtections(); + + expect(fleetServices.packagePolicy.list as jest.Mock).not.toHaveBeenCalled(); + expect(logger.info).toHaveBeenLastCalledWith( + 'App feature [endpoint_policy_protections] is enabled. Nothing to do!' + ); + }); + }); + + describe('and `endpointPolicyProtections` is disabled', () => { + let policyGenerator: FleetPackagePolicyGenerator; + let page1Items: PolicyData[] = []; + let page2Items: PolicyData[] = []; + let bulkUpdateResponse: PromiseResolvedValue>; + + const generatePolicyMock = (withDisabledProtections = false): PolicyData => { + const policy = policyGenerator.generateEndpointPackagePolicy(); + + if (!withDisabledProtections) { + return policy; + } + + policy.inputs[0].config.policy.value = ensureOnlyEventCollectionIsAllowed( + policy.inputs[0].config.policy.value + ); + + return policy; + }; + + beforeEach(() => { + policyGenerator = new FleetPackagePolicyGenerator('seed'); + const packagePolicyListSrv = fleetServices.packagePolicy.list as jest.Mock; + + appFeatures = createAppFeaturesMock( + ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections') + ); + + page1Items = [generatePolicyMock(), generatePolicyMock(true)]; + page2Items = [generatePolicyMock(true), generatePolicyMock()]; + + packagePolicyListSrv + .mockImplementationOnce(async () => { + return { + total: 1500, + page: 1, + perPage: 1000, + items: page1Items, + }; + }) + .mockImplementationOnce(async () => { + return { + total: 1500, + page: 2, + perPage: 1000, + items: page2Items, + }; + }); + + bulkUpdateResponse = { + updatedPolicies: [page1Items[0], page2Items[1]], + failedPolicies: [], + }; + + (fleetServices.packagePolicy.bulkUpdate as jest.Mock).mockImplementation(async () => { + return bulkUpdateResponse; + }); + }); + + it('should update only policies that have protections turn on', async () => { + await callTurnOffPolicyProtections(); + + expect(fleetServices.packagePolicy.list as jest.Mock).toHaveBeenCalledTimes(2); + expect(fleetServices.packagePolicy.bulkUpdate as jest.Mock).toHaveBeenCalledWith( + fleetServices.internalSoClient, + esClient, + [ + expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![0].id }), + expect.objectContaining({ id: bulkUpdateResponse.updatedPolicies![1].id }), + ], + { user: { username: 'elastic' } } + ); + expect(logger.info).toHaveBeenCalledWith( + 'Found 2 policies that need updates:\n' + + `Policy [${bulkUpdateResponse.updatedPolicies![0].id}][${ + bulkUpdateResponse.updatedPolicies![0].name + }] updated to disable protections. Trigger: [property [mac.malware.mode] is set to [prevent]]\n` + + `Policy [${bulkUpdateResponse.updatedPolicies![1].id}][${ + bulkUpdateResponse.updatedPolicies![1].name + }] updated to disable protections. Trigger: [property [mac.malware.mode] is set to [prevent]]` + ); + expect(logger.info).toHaveBeenCalledWith('Done. All updates applied successfully'); + }); + + it('should log failures', async () => { + bulkUpdateResponse.failedPolicies.push({ + error: new Error('oh oh'), + packagePolicy: bulkUpdateResponse.updatedPolicies![0], + }); + await callTurnOffPolicyProtections(); + + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Done. 1 out of 2 failed to update:') + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts new file mode 100644 index 00000000000000..c4a63b8ec841cc --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_policy_protections.ts @@ -0,0 +1,107 @@ +/* + * 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 { Logger, ElasticsearchClient } from '@kbn/core/server'; +import type { UpdatePackagePolicy } from '@kbn/fleet-plugin/common'; +import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import { + isPolicySetToEventCollectionOnly, + ensureOnlyEventCollectionIsAllowed, +} from '../../../common/endpoint/models/policy_config_helpers'; +import type { PolicyData } from '../../../common/endpoint/types'; +import { AppFeatureSecurityKey } from '../../../common/types/app_features'; +import type { EndpointInternalFleetServicesInterface } from '../services/fleet'; +import type { AppFeatures } from '../../lib/app_features'; +import { getPolicyDataForUpdate } from '../../../common/endpoint/service/policy'; + +export const turnOffPolicyProtectionsIfNotSupported = async ( + esClient: ElasticsearchClient, + fleetServices: EndpointInternalFleetServicesInterface, + appFeaturesService: AppFeatures, + logger: Logger +): Promise => { + const log = logger.get('endpoint', 'policyProtections'); + + if (appFeaturesService.isEnabled(AppFeatureSecurityKey.endpointPolicyProtections)) { + log.info( + `App feature [${AppFeatureSecurityKey.endpointPolicyProtections}] is enabled. Nothing to do!` + ); + + return; + } + + log.info( + `App feature [${AppFeatureSecurityKey.endpointPolicyProtections}] is disabled. Checking endpoint integration policies for compliance` + ); + + const { packagePolicy, internalSoClient, endpointPolicyKuery } = fleetServices; + const updates: UpdatePackagePolicy[] = []; + const messages: string[] = []; + const perPage = 1000; + let hasMoreData = true; + let total = 0; + let page = 1; + + do { + const currentPage = page++; + const { items, total: totalPolicies } = await packagePolicy.list(internalSoClient, { + page: currentPage, + kuery: endpointPolicyKuery, + perPage, + }); + + total = totalPolicies; + hasMoreData = currentPage * perPage < total; + + for (const item of items) { + const integrationPolicy = item as PolicyData; + const policySettings = integrationPolicy.inputs[0].config.policy.value; + const { message, isOnlyCollectingEvents } = isPolicySetToEventCollectionOnly(policySettings); + + if (!isOnlyCollectingEvents) { + messages.push( + `Policy [${integrationPolicy.id}][${integrationPolicy.name}] updated to disable protections. Trigger: [${message}]` + ); + + integrationPolicy.inputs[0].config.policy.value = + ensureOnlyEventCollectionIsAllowed(policySettings); + + updates.push({ + ...getPolicyDataForUpdate(integrationPolicy), + id: integrationPolicy.id, + }); + } + } + } while (hasMoreData); + + if (updates.length > 0) { + log.info(`Found ${updates.length} policies that need updates:\n${messages.join('\n')}`); + + const bulkUpdateResponse = await fleetServices.packagePolicy.bulkUpdate( + internalSoClient, + esClient, + updates, + { + user: { username: 'elastic' } as AuthenticatedUser, + } + ); + + log.debug(`Bulk update response:\n${JSON.stringify(bulkUpdateResponse, null, 2)}`); + + if (bulkUpdateResponse.failedPolicies.length > 0) { + log.error( + `Done. ${bulkUpdateResponse.failedPolicies.length} out of ${ + updates.length + } failed to update:\n${JSON.stringify(bulkUpdateResponse.failedPolicies, null, 2)}` + ); + } else { + log.info(`Done. All updates applied successfully`); + } + } else { + log.info(`Done. Checked ${total} policies and no updates needed`); + } +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index f38d96cbf70639..5a3c9ee2297ac9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -71,6 +71,7 @@ import type { EndpointAuthz } from '../../common/endpoint/types/authz'; import { EndpointFleetServicesFactory } from './services/fleet'; import { createLicenseServiceMock } from '../../common/license/mocks'; import { createFeatureUsageServiceMock } from './services/feature_usage/mocks'; +import { createAppFeaturesMock } from '../lib/app_features/mocks'; /** * Creates a mocked EndpointAppContext. @@ -163,6 +164,8 @@ export const createMockEndpointAppContextServiceStartContract = }, savedObjectsStart ); + const experimentalFeatures = config.experimentalFeatures; + const appFeatures = createAppFeaturesMock(undefined, experimentalFeatures, undefined, logger); packagePolicyService.list.mockImplementation(async (_, options) => { return { @@ -207,11 +210,12 @@ export const createMockEndpointAppContextServiceStartContract = cases: casesMock, cloud: cloudMock.createSetup(), featureUsageService: createFeatureUsageServiceMock(), - experimentalFeatures: createMockConfig().experimentalFeatures, + experimentalFeatures, messageSigningService: createMessageSigningServiceMock(), actionCreateService: undefined, createFleetActionsClient: jest.fn((_) => fleetActionsClientMock), esClient: elasticsearchClientMock.createElasticsearchClient(), + appFeatures, }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.ts b/x-pack/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.ts index 658ff9f2a327ed..1f3df9d6a67d3a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/fleet/endpoint_fleet_services_factory.ts @@ -13,6 +13,8 @@ import type { PackagePolicyClient, PackageClient, } from '@kbn/fleet-plugin/server'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; +import { createInternalSoClient } from '../../utils/create_internal_so_client'; import { createInternalReadonlySoClient } from '../../utils/create_internal_readonly_so_client'; export interface EndpointFleetServicesFactoryInterface { @@ -42,7 +44,10 @@ export class EndpointFleetServicesFactory implements EndpointFleetServicesFactor packages: packageService.asInternalUser, packagePolicy, + endpointPolicyKuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "endpoint"`, + internalReadonlySoClient: createInternalReadonlySoClient(this.savedObjectsStart), + internalSoClient: createInternalSoClient(this.savedObjectsStart), }; } } @@ -55,6 +60,8 @@ export interface EndpointFleetServicesInterface { agentPolicy: AgentPolicyServiceInterface; packages: PackageClient; packagePolicy: PackagePolicyClient; + /** The `kuery` that can be used to filter for Endpoint integration policies */ + endpointPolicyKuery: string; } export interface EndpointInternalFleetServicesInterface extends EndpointFleetServicesInterface { @@ -62,4 +69,7 @@ export interface EndpointInternalFleetServicesInterface extends EndpointFleetSer * An internal SO client (readonly) that can be used with the Fleet services that require it */ internalReadonlySoClient: SavedObjectsClientContract; + + /** Internal SO client. USE ONLY WHEN ABSOLUTELY NEEDED. Else, use the `internalReadonlySoClient` */ + internalSoClient: SavedObjectsClientContract; } diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.ts b/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.ts index d8bf7badec846b..b621222e79c0ab 100644 --- a/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_readonly_so_client.ts @@ -5,12 +5,8 @@ * 2.0. */ -import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; -import type { - KibanaRequest, - SavedObjectsClientContract, - SavedObjectsServiceStart, -} from '@kbn/core/server'; +import type { SavedObjectsClientContract, SavedObjectsServiceStart } from '@kbn/core/server'; +import { createInternalSoClient } from './create_internal_so_client'; import { EndpointError } from '../../../common/endpoint/errors'; type SavedObjectsClientContractKeys = keyof SavedObjectsClientContract; @@ -37,18 +33,7 @@ export class InternalReadonlySoClientMethodNotAllowedError extends EndpointError export const createInternalReadonlySoClient = ( savedObjectsServiceStart: SavedObjectsServiceStart ): SavedObjectsClientContract => { - const fakeRequest = { - headers: {}, - getBasePath: () => '', - path: '/', - route: { settings: {} }, - url: { href: {} }, - raw: { req: { url: '/' } }, - } as unknown as KibanaRequest; - - const internalSoClient = savedObjectsServiceStart.getScopedClient(fakeRequest, { - excludedExtensions: [SECURITY_EXTENSION_ID], - }); + const internalSoClient = createInternalSoClient(savedObjectsServiceStart); return new Proxy(internalSoClient, { get( diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_so_client.ts b/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_so_client.ts new file mode 100644 index 00000000000000..88e0d7a70a4c39 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/utils/create_internal_so_client.ts @@ -0,0 +1,27 @@ +/* + * 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 { SavedObjectsServiceStart } from '@kbn/core-saved-objects-server'; +import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; +import type { KibanaRequest, SavedObjectsClientContract } from '@kbn/core/server'; + +export const createInternalSoClient = ( + savedObjectsServiceStart: SavedObjectsServiceStart +): SavedObjectsClientContract => { + const fakeRequest = { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { href: {} }, + raw: { req: { url: '/' } }, + } as unknown as KibanaRequest; + + return savedObjectsServiceStart.getScopedClient(fakeRequest, { + excludedExtensions: [SECURITY_EXTENSION_ID], + }); +}; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 54258638f12301..f26531296b6a21 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -54,6 +54,9 @@ import { createMockPolicyData } from '../endpoint/services/feature_usage/mocks'; import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../common/endpoint/service/artifacts/constants'; import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { disableProtections } from '../../common/endpoint/models/policy_config_helpers'; +import type { AppFeatures } from '../lib/app_features'; +import { createAppFeaturesMock } from '../lib/app_features/mocks'; +import { ALL_APP_FEATURE_KEYS } from '../../common'; jest.mock('uuid', () => ({ v4: (): string => 'NEW_UUID', @@ -74,6 +77,7 @@ describe('ingest_integration tests ', () => { }); const generator = new EndpointDocGenerator(); const cloudService = cloudMock.createSetup(); + let appFeatures: AppFeatures; beforeEach(() => { endpointAppContextMock = createMockEndpointAppContextServiceStartContract(); @@ -82,6 +86,7 @@ describe('ingest_integration tests ', () => { licenseEmitter = new Subject(); licenseService = new LicenseService(); licenseService.start(licenseEmitter); + appFeatures = endpointAppContextMock.appFeatures; jest .spyOn(endpointAppContextMock.endpointMetadataService, 'getFleetEndpointPackagePolicy') @@ -129,7 +134,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.alerting, licenseService, exceptionListClient, - cloudService + cloudService, + appFeatures ); return callback( @@ -363,6 +369,7 @@ describe('ingest_integration tests ', () => { ); }); }); + describe('package policy update callback (when the license is below platinum)', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -379,7 +386,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.featureUsageService, endpointAppContextMock.endpointMetadataService, cloudService, - esClient + esClient, + appFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -397,7 +405,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.featureUsageService, endpointAppContextMock.endpointMetadataService, cloudService, - esClient + esClient, + appFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -419,6 +428,7 @@ describe('ingest_integration tests ', () => { beforeEach(() => { licenseEmitter.next(Platinum); // set license level to platinum }); + it('updates successfully when paid features are turned on', async () => { const mockPolicy = policyFactory(); mockPolicy.windows.popup.malware.message = 'paid feature'; @@ -429,7 +439,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.featureUsageService, endpointAppContextMock.endpointMetadataService, cloudService, - esClient + esClient, + appFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -442,6 +453,50 @@ describe('ingest_integration tests ', () => { ); expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy); }); + + it('should turn off protections if endpointPolicyProtections appFeature is disabled', async () => { + appFeatures = createAppFeaturesMock( + ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections') + ); + const callback = getPackagePolicyUpdateCallback( + endpointAppContextMock.logger, + licenseService, + endpointAppContextMock.featureUsageService, + endpointAppContextMock.endpointMetadataService, + cloudService, + esClient, + appFeatures + ); + + const updatedPolicy = await callback( + generator.generatePolicyPackagePolicy(), + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + + expect(updatedPolicy.inputs?.[0]?.config?.policy.value).toMatchObject({ + linux: { + behavior_protection: { mode: 'off' }, + malware: { mode: 'off' }, + memory_protection: { mode: 'off' }, + }, + mac: { + behavior_protection: { mode: 'off' }, + malware: { mode: 'off' }, + memory_protection: { mode: 'off' }, + }, + windows: { + antivirus_registration: { enabled: false }, + attack_surface_reduction: { credential_hardening: { enabled: false } }, + behavior_protection: { mode: 'off' }, + malware: { blocklist: false }, + memory_protection: { mode: 'off' }, + ransomware: { mode: 'off' }, + }, + }); + }); }); describe('package policy update callback when meta fields should be updated', () => { @@ -486,7 +541,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.featureUsageService, endpointAppContextMock.endpointMetadataService, cloudService, - esClient + esClient, + appFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); @@ -520,7 +576,8 @@ describe('ingest_integration tests ', () => { endpointAppContextMock.featureUsageService, endpointAppContextMock.endpointMetadataService, cloudService, - esClient + esClient, + appFeatures ); const policyConfig = generator.generatePolicyPackagePolicy(); // values should be updated diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index b897441fe1e049..04bc9afa6d3a1e 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -22,6 +22,12 @@ import type { } from '@kbn/fleet-plugin/common'; import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types'; +import { AppFeatureSecurityKey } from '../../common/types/app_features'; +import { + isPolicySetToEventCollectionOnly, + ensureOnlyEventCollectionIsAllowed, +} from '../../common/endpoint/models/policy_config_helpers'; +import type { AppFeatures } from '../lib/app_features'; import type { NewPolicyData, PolicyConfig } from '../../common/endpoint/types'; import type { LicenseService } from '../../common/license'; import type { ManifestManager } from '../endpoint/services'; @@ -72,7 +78,8 @@ export const getPackagePolicyCreateCallback = ( alerts: AlertsStartContract, licenseService: LicenseService, exceptionsClient: ExceptionListClient | undefined, - cloud: CloudSetup + cloud: CloudSetup, + appFeatures: AppFeatures ): PostPackagePolicyCreateCallback => { return async ( newPackagePolicy, @@ -140,7 +147,8 @@ export const getPackagePolicyCreateCallback = ( licenseService, endpointIntegrationConfig, cloud, - esClientInfo + esClientInfo, + appFeatures ); return { @@ -175,31 +183,38 @@ export const getPackagePolicyUpdateCallback = ( featureUsageService: FeatureUsageService, endpointMetadataService: EndpointMetadataService, cloud: CloudSetup, - esClient: ElasticsearchClient + esClient: ElasticsearchClient, + appFeatures: AppFeatures ): PutPackagePolicyUpdateCallback => { return async (newPackagePolicy: NewPackagePolicy): Promise => { if (!isEndpointPackagePolicy(newPackagePolicy)) { return newPackagePolicy; } + const endpointIntegrationData = newPackagePolicy as NewPolicyData; + // Validate that Endpoint Security policy is valid against current license validatePolicyAgainstLicense( // The cast below is needed in order to ensure proper typing for // the policy configuration specific for endpoint - newPackagePolicy.inputs[0].config?.policy?.value as PolicyConfig, + endpointIntegrationData.inputs[0].config?.policy?.value as PolicyConfig, licenseService, logger ); - notifyProtectionFeatureUsage(newPackagePolicy, featureUsageService, endpointMetadataService); + notifyProtectionFeatureUsage( + endpointIntegrationData, + featureUsageService, + endpointMetadataService + ); - const newEndpointPackagePolicy = newPackagePolicy.inputs[0].config?.policy + const newEndpointPackagePolicy = endpointIntegrationData.inputs[0].config?.policy ?.value as PolicyConfig; const esClientInfo: InfoResponse = await esClient.info(); if ( - newPackagePolicy.inputs[0].config?.policy?.value && + endpointIntegrationData.inputs[0].config?.policy?.value && shouldUpdateMetaValues( newEndpointPackagePolicy, licenseService.getLicenseType(), @@ -214,10 +229,25 @@ export const getPackagePolicyUpdateCallback = ( newEndpointPackagePolicy.meta.cluster_name = esClientInfo.cluster_name; newEndpointPackagePolicy.meta.cluster_uuid = esClientInfo.cluster_uuid; newEndpointPackagePolicy.meta.license_uid = licenseService.getLicenseUID(); - newPackagePolicy.inputs[0].config.policy.value = newEndpointPackagePolicy; + + endpointIntegrationData.inputs[0].config.policy.value = newEndpointPackagePolicy; + } + + // If no Policy Protection allowed (ex. serverless) + const eventsOnlyPolicy = isPolicySetToEventCollectionOnly(newEndpointPackagePolicy); + if ( + !appFeatures.isEnabled(AppFeatureSecurityKey.endpointPolicyProtections) && + !eventsOnlyPolicy.isOnlyCollectingEvents + ) { + logger.warn( + `Endpoint integration policy [${endpointIntegrationData.id}][${endpointIntegrationData.name}] adjusted due to [endpointPolicyProtections] appFeature not being enabled. Trigger [${eventsOnlyPolicy.message}]` + ); + + endpointIntegrationData.inputs[0].config.policy.value = + ensureOnlyEventCollectionIsAllowed(newEndpointPackagePolicy); } - return newPackagePolicy; + return endpointIntegrationData; }; }; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts index b707199aa47387..9208f9f7f22cd5 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts @@ -19,6 +19,9 @@ import type { PolicyCreateCloudConfig, PolicyCreateEndpointConfig, } from '../types'; +import type { AppFeatures } from '../../lib/app_features'; +import { createAppFeaturesMock } from '../../lib/app_features/mocks'; +import { ALL_APP_FEATURE_KEYS } from '../../../common'; describe('Create Default Policy tests ', () => { const cloud = cloudMock.createSetup(); @@ -28,6 +31,7 @@ describe('Create Default Policy tests ', () => { const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold', uid: '' } }); let licenseEmitter: Subject; let licenseService: LicenseService; + let appFeatures: AppFeatures; const createDefaultPolicyCallback = async ( config: AnyPolicyCreateConfig | undefined @@ -35,7 +39,7 @@ describe('Create Default Policy tests ', () => { const esClientInfo = await elasticsearchServiceMock.createClusterClient().asInternalUser.info(); esClientInfo.cluster_name = ''; esClientInfo.cluster_uuid = ''; - return createDefaultPolicy(licenseService, config, cloud, esClientInfo); + return createDefaultPolicy(licenseService, config, cloud, esClientInfo, appFeatures); }; beforeEach(() => { @@ -43,7 +47,9 @@ describe('Create Default Policy tests ', () => { licenseService = new LicenseService(); licenseService.start(licenseEmitter); licenseEmitter.next(Platinum); // set license level to platinum + appFeatures = createAppFeaturesMock(); }); + describe('When no config is set', () => { it('Should return PolicyConfig for events only when license is at least platinum', async () => { const defaultPolicy = policyFactory(); @@ -174,6 +180,7 @@ describe('Create Default Policy tests ', () => { }); }); }); + it('Should return process, file and network events enabled when preset is EDR Essential', async () => { const config = createEndpointConfig({ preset: 'EDREssential' }); const policy = await createDefaultPolicyCallback(config); @@ -190,6 +197,7 @@ describe('Create Default Policy tests ', () => { }); }); }); + it('Should return the default config when preset is EDR Complete', async () => { const config = createEndpointConfig({ preset: 'EDRComplete' }); const policy = await createDefaultPolicyCallback(config); @@ -199,7 +207,37 @@ describe('Create Default Policy tests ', () => { defaultPolicy.meta.cloud = true; expect(policy).toMatchObject(defaultPolicy); }); + + it('should set policy to event collection only if endpointPolicyProtections appFeature is disabled', async () => { + appFeatures = createAppFeaturesMock( + ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_policy_protections') + ); + + await expect( + createDefaultPolicyCallback(createEndpointConfig({ preset: 'EDRComplete' })) + ).resolves.toMatchObject({ + linux: { + behavior_protection: { mode: 'off' }, + malware: { mode: 'off' }, + memory_protection: { mode: 'off' }, + }, + mac: { + behavior_protection: { mode: 'off' }, + malware: { mode: 'off' }, + memory_protection: { mode: 'off' }, + }, + windows: { + antivirus_registration: { enabled: false }, + attack_surface_reduction: { credential_hardening: { enabled: false } }, + behavior_protection: { mode: 'off' }, + malware: { blocklist: false }, + memory_protection: { mode: 'off' }, + ransomware: { mode: 'off' }, + }, + }); + }); }); + describe('When cloud config is set', () => { const createCloudConfig = (): PolicyCreateCloudConfig => ({ type: 'cloud', diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts index d7c3994c05dc92..db053fd5c3b0e2 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts @@ -7,6 +7,8 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types'; +import { AppFeatureSecurityKey } from '../../../common/types/app_features'; +import type { AppFeatures } from '../../lib/app_features'; import { policyFactory as policyConfigFactory, policyFactoryWithoutPaidFeatures as policyConfigFactoryWithoutPaidFeatures, @@ -20,7 +22,10 @@ import { ENDPOINT_CONFIG_PRESET_NGAV, ENDPOINT_CONFIG_PRESET_DATA_COLLECTION, } from '../constants'; -import { disableProtections } from '../../../common/endpoint/models/policy_config_helpers'; +import { + disableProtections, + ensureOnlyEventCollectionIsAllowed, +} from '../../../common/endpoint/models/policy_config_helpers'; /** * Create the default endpoint policy based on the current license and configuration type @@ -29,7 +34,8 @@ export const createDefaultPolicy = ( licenseService: LicenseService, config: AnyPolicyCreateConfig | undefined, cloud: CloudSetup, - esClientInfo: InfoResponse + esClientInfo: InfoResponse, + appFeatures: AppFeatures ): PolicyConfig => { const factoryPolicy = policyConfigFactory(); @@ -44,15 +50,21 @@ export const createDefaultPolicy = ( : factoryPolicy.meta.cluster_uuid; factoryPolicy.meta.license_uid = licenseService.getLicenseUID(); - const defaultPolicyPerType = + let defaultPolicyPerType: PolicyConfig = config?.type === 'cloud' ? getCloudPolicyConfig(factoryPolicy) : getEndpointPolicyWithIntegrationConfig(factoryPolicy, config); - // Apply license limitations in the final step, so it's not overriden (see malware popup) - return licenseService.isPlatinumPlus() - ? defaultPolicyPerType - : policyConfigFactoryWithoutPaidFeatures(defaultPolicyPerType); + if (!licenseService.isPlatinumPlus()) { + defaultPolicyPerType = policyConfigFactoryWithoutPaidFeatures(defaultPolicyPerType); + } + + // If no Policy Protection allowed (ex. serverless) + if (!appFeatures.isEnabled(AppFeatureSecurityKey.endpointPolicyProtections)) { + defaultPolicyPerType = ensureOnlyEventCollectionIsAllowed(defaultPolicyPerType); + } + + return defaultPolicyPerType; }; /** diff --git a/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts index 69c6c33d335c3e..edeec0d533a40a 100644 --- a/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts +++ b/x-pack/plugins/security_solution/server/lib/app_features/app_features.ts @@ -59,7 +59,7 @@ export class AppFeatures { return this.appFeatures.has(appFeatureKey); } - private registerEnabledKibanaFeatures() { + protected registerEnabledKibanaFeatures() { if (this.featuresSetup == null) { throw new Error( 'Cannot sync kibana features as featuresSetup is not present. Did you call init?' diff --git a/x-pack/plugins/security_solution/server/lib/app_features/mocks.ts b/x-pack/plugins/security_solution/server/lib/app_features/mocks.ts new file mode 100644 index 00000000000000..1a5efc9c64e37b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/app_features/mocks.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; +import { AppFeatures } from './app_features'; +import type { AppFeatureKeys, ExperimentalFeatures } from '../../../common'; +import { ALL_APP_FEATURE_KEYS, allowedExperimentalValues } from '../../../common'; + +class AppFeaturesMock extends AppFeatures { + protected registerEnabledKibanaFeatures() { + // NOOP + } +} + +export const createAppFeaturesMock = ( + /** What features keys should be enabled. Default is all */ + enabledFeatureKeys: AppFeatureKeys = [...ALL_APP_FEATURE_KEYS], + experimentalFeatures: ExperimentalFeatures = { ...allowedExperimentalValues }, + featuresPluginSetupContract: FeaturesPluginSetup = featuresPluginMock.createSetup(), + logger: Logger = loggingSystemMock.create().get('appFeatureMock') +) => { + const appFeatures = new AppFeaturesMock(logger, experimentalFeatures); + + appFeatures.init(featuresPluginSetupContract); + + if (enabledFeatureKeys) { + appFeatures.set(enabledFeatureKeys); + } + + return appFeatures; +}; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index e64a5808eb3ddc..4e7beb558d4de9 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -17,6 +17,7 @@ import { Dataset } from '@kbn/rule-registry-plugin/server'; import type { ListPluginSetup } from '@kbn/lists-plugin/server'; import type { ILicense } from '@kbn/licensing-plugin/server'; +import { turnOffPolicyProtectionsIfNotSupported } from './endpoint/migrations/turn_off_policy_protections'; import { endpointSearchStrategyProvider } from './search_strategy/endpoint'; import { getScheduleNotificationResponseActionsService } from './lib/detection_engine/rule_response_actions/schedule_notification_response_actions'; import { siemGuideId, siemGuideConfig } from '../common/guided_onboarding/siem_guide_config'; @@ -438,6 +439,15 @@ export class Plugin implements ISecuritySolutionPlugin { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion plugins.fleet!; let manifestManager: ManifestManager | undefined; + const endpointFleetServicesFactory = new EndpointFleetServicesFactory( + { + agentService, + packageService, + packagePolicyService, + agentPolicyService, + }, + core.savedObjects + ); this.licensing$ = plugins.licensing.license$; @@ -459,17 +469,23 @@ export class Plugin implements ISecuritySolutionPlugin { esClient: core.elasticsearch.client.asInternalUser, }); - // Migrate artifacts to fleet and then start the minifest task after that is done + // Migrate artifacts to fleet and then start the manifest task after that is done plugins.fleet.fleetSetupCompleted().then(() => { - logger.info('Dependent plugin setup complete - Starting ManifestTask'); - if (this.manifestTask) { + logger.info('Dependent plugin setup complete - Starting ManifestTask'); this.manifestTask.start({ taskManager, }); } else { logger.error(new Error('User artifacts task not available.')); } + + turnOffPolicyProtectionsIfNotSupported( + core.elasticsearch.client.asInternalUser, + endpointFleetServicesFactory.asInternalUser(), + this.appFeatures, + logger + ); }); // License related start @@ -493,15 +509,7 @@ export class Plugin implements ISecuritySolutionPlugin { packagePolicyService, logger ), - endpointFleetServicesFactory: new EndpointFleetServicesFactory( - { - agentService, - packageService, - packagePolicyService, - agentPolicyService, - }, - core.savedObjects - ), + endpointFleetServicesFactory, security: plugins.security, alerting: plugins.alerting, config: this.config, @@ -522,6 +530,7 @@ export class Plugin implements ISecuritySolutionPlugin { ), createFleetActionsClient, esClient: core.elasticsearch.client.asInternalUser, + appFeatures: this.appFeatures, }); this.telemetryReceiver.start(