diff --git a/x-pack/plugins/cloud_security_posture/server/plugin.test.ts b/x-pack/plugins/cloud_security_posture/server/plugin.test.ts index 4a3b85b41a159..10edb5859ddee 100644 --- a/x-pack/plugins/cloud_security_posture/server/plugin.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.test.ts @@ -32,10 +32,10 @@ import { UpdatePackagePolicy, } from '@kbn/fleet-plugin/common'; import { - ExternalCallback, FleetStartContract, PostPackagePolicyPostDeleteCallback, PostPackagePolicyPostCreateCallback, + ExternalCallback, } from '@kbn/fleet-plugin/server'; import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../common/constants'; import Chance from 'chance'; diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index ec8ada164623d..fb6dd7d075cea 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -157,6 +157,7 @@ export const createMockAgentPolicyService = (): jest.Mocked { diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index ba348e9e639e2..68237bb4e0ac9 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -195,6 +195,12 @@ export const createAgentPolicyHandler: FleetRequestHandler< body, }); } catch (error) { + if (error.statusCode) { + return response.customError({ + statusCode: error.statusCode, + body: { message: error.message }, + }); + } return defaultFleetErrorHandler({ error, response }); } }; @@ -229,6 +235,12 @@ export const updateAgentPolicyHandler: FleetRequestHandler< body, }); } catch (error) { + if (error.statusCode) { + return response.customError({ + statusCode: error.statusCode, + body: { message: error.message }, + }); + } return defaultFleetErrorHandler({ error, response }); } }; diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index cb64b0e543d2f..6795d81f28946 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -44,6 +44,9 @@ import type { FullAgentPolicy, ListWithKuery, NewPackagePolicy, + PostAgentPolicyCreateCallback, + PostAgentPolicyUpdateCallback, + ExternalCallback, } from '../types'; import { getAllowedOutputTypeForPolicy, @@ -234,6 +237,43 @@ class AgentPolicyService { return policyHasSyntheticsIntegration(agentPolicy); } + public async runExternalCallbacks( + externalCallbackType: ExternalCallback[0], + agentPolicy: NewAgentPolicy | Partial + ): Promise> { + const logger = appContextService.getLogger(); + logger.debug(`Running external callbacks for ${externalCallbackType}`); + try { + const externalCallbacks = appContextService.getExternalCallbacks(externalCallbackType); + let newAgentPolicy = agentPolicy; + + if (externalCallbacks && externalCallbacks.size > 0) { + let updatedNewAgentPolicy = newAgentPolicy; + for (const callback of externalCallbacks) { + let result; + if (externalCallbackType === 'agentPolicyCreate') { + result = await (callback as PostAgentPolicyCreateCallback)( + newAgentPolicy as NewAgentPolicy + ); + updatedNewAgentPolicy = result; + } + if (externalCallbackType === 'agentPolicyUpdate') { + result = await (callback as PostAgentPolicyUpdateCallback)( + newAgentPolicy as Partial + ); + updatedNewAgentPolicy = result; + } + } + newAgentPolicy = updatedNewAgentPolicy; + } + return newAgentPolicy; + } catch (error) { + logger.error(`Error running external callbacks for ${externalCallbackType}`); + logger.error(error); + throw error; + } + } + public async create( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, @@ -254,7 +294,7 @@ class AgentPolicyService { id: options.id, savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE, }); - + await this.runExternalCallbacks('agentPolicyCreate', agentPolicy); this.checkTamperProtectionLicense(agentPolicy); const logger = appContextService.getLogger(); @@ -519,7 +559,14 @@ class AgentPolicyService { if (!existingAgentPolicy) { throw new AgentPolicyNotFoundError('Agent policy not found'); } - + try { + await this.runExternalCallbacks('agentPolicyUpdate', agentPolicy); + } catch (error) { + logger.error(`Error running external callbacks for agentPolicyUpdate`); + if (error.apiPassThrough) { + throw error; + } + } this.checkTamperProtectionLicense(agentPolicy); await this.checkForValidUninstallToken(agentPolicy, id); diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 9e40bcd64c8c8..65a0fb6c08f35 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -42,6 +42,8 @@ import type { PostPackagePolicyPostDeleteCallback, PostPackagePolicyPostCreateCallback, PutPackagePolicyUpdateCallback, + PostAgentPolicyCreateCallback, + PostAgentPolicyUpdateCallback, } from '../types'; import type { FleetAppContext } from '../plugin'; import type { TelemetryEventsSender } from '../telemetry/sender'; @@ -245,7 +247,11 @@ class AppContextService { type: T ): | Set< - T extends 'packagePolicyCreate' + T extends 'agentPolicyCreate' + ? PostAgentPolicyCreateCallback + : T extends 'agentPolicyUpdate' + ? PostAgentPolicyUpdateCallback + : T extends 'packagePolicyCreate' ? PostPackagePolicyCreateCallback : T extends 'packagePolicyDelete' ? PostPackagePolicyDeleteCallback @@ -258,7 +264,11 @@ class AppContextService { | undefined { if (this.externalCallbacks) { return this.externalCallbacks.get(type) as Set< - T extends 'packagePolicyCreate' + T extends 'agentPolicyCreate' + ? PostAgentPolicyCreateCallback + : T extends 'agentPolicyUpdate' + ? PostAgentPolicyUpdateCallback + : T extends 'packagePolicyCreate' ? PostPackagePolicyCreateCallback : T extends 'packagePolicyDelete' ? PostPackagePolicyDeleteCallback diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index 323d091cae1d2..8625644eaf685 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -33,6 +33,7 @@ export interface AgentPolicyServiceInterface { list: typeof agentPolicyService['list']; getFullAgentPolicy: typeof agentPolicyService['getFullAgentPolicy']; getByIds: typeof agentPolicyService['getByIDs']; + bumpRevision: typeof agentPolicyService['bumpRevision']; } // Agent services diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index bf70ef7652d9a..fa81ae68a0445 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -283,6 +283,7 @@ jest.mock('./app_context', () => ({ getUninstallTokenService: () => ({ generateTokenForPolicyId: jest.fn(), }), + getExternalCallbacks: jest.fn(), }, })); diff --git a/x-pack/plugins/fleet/server/types/extensions.ts b/x-pack/plugins/fleet/server/types/extensions.ts index ca5a0d84c958e..594e16f619556 100644 --- a/x-pack/plugins/fleet/server/types/extensions.ts +++ b/x-pack/plugins/fleet/server/types/extensions.ts @@ -16,6 +16,8 @@ import type { UpdatePackagePolicy, PackagePolicy, DeletePackagePoliciesResponse, + NewAgentPolicy, + AgentPolicy, } from '../../common/types'; export type PostPackagePolicyDeleteCallback = ( @@ -58,6 +60,14 @@ export type PutPackagePolicyUpdateCallback = ( request?: KibanaRequest ) => Promise; +export type PostAgentPolicyCreateCallback = ( + agentPolicy: NewAgentPolicy +) => Promise; + +export type PostAgentPolicyUpdateCallback = ( + agentPolicy: Partial +) => Promise>; + export type ExternalCallbackCreate = ['packagePolicyCreate', PostPackagePolicyCreateCallback]; export type ExternalCallbackPostCreate = [ 'packagePolicyPostCreate', @@ -71,6 +81,15 @@ export type ExternalCallbackPostDelete = [ ]; export type ExternalCallbackUpdate = ['packagePolicyUpdate', PutPackagePolicyUpdateCallback]; +export type ExternalCallbackAgentPolicyCreate = [ + 'agentPolicyCreate', + PostAgentPolicyCreateCallback +]; +export type ExternalCallbackAgentPolicyUpdate = [ + 'agentPolicyUpdate', + PostAgentPolicyUpdateCallback +]; + /** * Callbacks supported by the Fleet plugin */ @@ -79,6 +98,8 @@ export type ExternalCallback = | ExternalCallbackPostCreate | ExternalCallbackDelete | ExternalCallbackPostDelete - | ExternalCallbackUpdate; + | ExternalCallbackUpdate + | ExternalCallbackAgentPolicyCreate + | ExternalCallbackAgentPolicyUpdate; export type ExternalCallbacksStorage = Map>; diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/api/agent_policy_settings_complete.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/api/agent_policy_settings_complete.cy.ts new file mode 100644 index 0000000000000..4b2b4dbf369d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/api/agent_policy_settings_complete.cy.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IndexedFleetEndpointPolicyResponse } from '../../../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { + createAgentPolicyTask, + createAgentPolicyWithAgentTamperProtectionsEnabled, + enableAgentTamperProtectionFeatureFlagInPolicy, + getEndpointIntegrationVersion, +} from '../../../../tasks/fleet'; +import { login } from '../../../../tasks/login'; + +describe( + 'Agent policy settings API operations on Complete', + { + tags: ['@serverless'], + env: { + ftrConfig: { + productTypes: [ + { product_line: 'security', product_tier: 'complete' }, + { product_line: 'endpoint', product_tier: 'complete' }, + ], + }, + }, + }, + () => { + let indexedPolicy: IndexedFleetEndpointPolicyResponse; + // let policy: PolicyData; + + beforeEach(() => { + getEndpointIntegrationVersion().then((version) => + createAgentPolicyTask(version).then((data) => { + indexedPolicy = data; + }) + ); + login(); + }); + + afterEach(() => { + if (indexedPolicy) { + cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy); + } + }); + + describe('Agent tamper protections', () => { + it('allow enabling the feature', () => { + enableAgentTamperProtectionFeatureFlagInPolicy(indexedPolicy.agentPolicies[0].id).then( + (response) => { + expect(response.status).to.equal(200); + expect(response.body.item.is_protected).to.equal(true); + } + ); + }); + it('throw error when trying to create agent policy', () => { + createAgentPolicyWithAgentTamperProtectionsEnabled().then((response) => { + expect(response.status).to.equal(200); + expect(response.body.item.is_protected).to.equal(false); // We don't allow creating a policy with the feature enabled + }); + }); + }); + } +); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/api/agent_policy_settings_essentials.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/api/agent_policy_settings_essentials.cy.ts new file mode 100644 index 0000000000000..9b24f0e2279ca --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/api/agent_policy_settings_essentials.cy.ts @@ -0,0 +1,76 @@ +/* + * 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 { UpdateAgentPolicyResponse } from '@kbn/fleet-plugin/common/types'; +import type { IndexedFleetEndpointPolicyResponse } from '../../../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { + createAgentPolicyTask, + createAgentPolicyWithAgentTamperProtectionsEnabled, + enableAgentTamperProtectionFeatureFlagInPolicy, + getEndpointIntegrationVersion, +} from '../../../../tasks/fleet'; +import { login } from '../../../../tasks/login'; + +describe( + 'Agent policy settings API operations on Essentials', + { + tags: ['@serverless'], + env: { + ftrConfig: { + productTypes: [ + { product_line: 'security', product_tier: 'essentials' }, + { product_line: 'endpoint', product_tier: 'essentials' }, + ], + }, + }, + }, + () => { + let indexedPolicy: IndexedFleetEndpointPolicyResponse; + + beforeEach(() => { + getEndpointIntegrationVersion().then((version) => + createAgentPolicyTask(version).then((data) => { + indexedPolicy = data; + }) + ); + login(); + }); + + afterEach(() => { + if (indexedPolicy) { + cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy); + } + }); + + describe('Agent tamper protections', () => { + it('throw error when trying to update agent policy settings', () => { + enableAgentTamperProtectionFeatureFlagInPolicy(indexedPolicy.agentPolicies[0].id, { + failOnStatusCode: false, + }).then((res) => { + const response = res as Cypress.Response; + expect(response.status).to.equal(403); + expect(response.body.message).to.equal( + 'Agent Tamper Protection is not allowed in current environment' + ); + }); + }); + it('throw error when trying to create agent policy', () => { + createAgentPolicyWithAgentTamperProtectionsEnabled({ failOnStatusCode: false }).then( + (res) => { + const response = res as Cypress.Response< + UpdateAgentPolicyResponse & { message: string } + >; + expect(response.status).to.equal(403); + expect(response.body.message).to.equal( + 'Agent Tamper Protection is not allowed in current environment' + ); + } + ); + }); + }); + } +); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts index aa2568268f020..e122eebedfc33 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/fleet.ts @@ -11,6 +11,7 @@ import type { GetInfoResponse, GetPackagePoliciesResponse, GetOneAgentPolicyResponse, + CreateAgentPolicyResponse, } from '@kbn/fleet-plugin/common'; import { agentRouteService, @@ -99,7 +100,30 @@ export const createAgentPolicyTask = ( ); }; -export const enableAgentTamperProtectionFeatureFlagInPolicy = (agentPolicyId: string) => { +export const createAgentPolicyWithAgentTamperProtectionsEnabled = ( + overwrite?: Record +) => { + return request({ + method: 'POST', + url: agentPolicyRouteService.getCreatePath(), + body: { + name: `With agent tamper protection enabled ${Math.random().toString(36).substring(2, 7)}`, + agent_features: [{ name: 'tamper_protection', enabled: true }], + is_protected: true, + description: 'test', + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + inactivity_timeout: 1209600, + }, + headers: { 'Elastic-Api-Version': API_VERSIONS.public.v1 }, + ...(overwrite ?? {}), + }); +}; + +export const enableAgentTamperProtectionFeatureFlagInPolicy = ( + agentPolicyId: string, + overwrite?: Record +) => { return request({ method: 'PUT', url: agentPolicyRouteService.getUpdatePath(agentPolicyId), @@ -113,6 +137,7 @@ export const enableAgentTamperProtectionFeatureFlagInPolicy = (agentPolicyId: st inactivity_timeout: 1209600, }, headers: { 'Elastic-Api-Version': API_VERSIONS.public.v1 }, + ...(overwrite ?? {}), }); }; 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 4a1c1332916db..fa4cd259652db 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 @@ -24,6 +24,8 @@ import type { PluginStartContract as AlertsPluginStartContract } from '@kbn/aler import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions/types'; import { + getAgentPolicyCreateCallback, + getAgentPolicyUpdateCallback, getPackagePolicyCreateCallback, getPackagePolicyDeleteCallback, getPackagePolicyPostCreateCallback, @@ -119,6 +121,15 @@ export class EndpointAppContextService { savedObjectsClient, } = dependencies; + registerIngestCallback( + 'agentPolicyCreate', + getAgentPolicyCreateCallback(logger, appFeaturesService) + ); + registerIngestCallback( + 'agentPolicyUpdate', + getAgentPolicyUpdateCallback(logger, appFeaturesService) + ); + registerIngestCallback( 'packagePolicyCreate', getPackagePolicyCreateCallback( diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.test.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.test.ts new file mode 100644 index 0000000000000..b54221f300e27 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.test.ts @@ -0,0 +1,167 @@ +/* + * 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 { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-features/keys'; +import type { AppFeaturesService } from '../../lib/app_features_service/app_features_service'; +import { createAppFeaturesServiceMock } from '../../lib/app_features_service/mocks'; +import { turnOffAgentPolicyFeatures } from './turn_off_agent_policy_features'; +import { FleetAgentPolicyGenerator } from '../../../common/endpoint/data_generators/fleet_agent_policy_generator'; +import type { AgentPolicy, GetAgentPoliciesResponseItem } from '@kbn/fleet-plugin/common'; + +describe('Turn Off Agent Policy Features Migration', () => { + let esClient: ElasticsearchClient; + let fleetServices: EndpointInternalFleetServicesInterface; + let appFeatureService: AppFeaturesService; + let logger: Logger; + + const callTurnOffAgentPolicyFeatures = () => + turnOffAgentPolicyFeatures(esClient, fleetServices, appFeatureService, logger); + + beforeEach(() => { + const endpointContextStartContract = createMockEndpointAppContextServiceStartContract(); + + ({ esClient, logger } = endpointContextStartContract); + + appFeatureService = endpointContextStartContract.appFeaturesService; + fleetServices = endpointContextStartContract.endpointFleetServicesFactory.asInternalUser(); + }); + + describe('and `agentTamperProtection` is enabled', () => { + it('should do nothing', async () => { + await callTurnOffAgentPolicyFeatures(); + + expect(fleetServices.agentPolicy.list as jest.Mock).not.toHaveBeenCalled(); + expect(logger.info).toHaveBeenLastCalledWith( + 'App feature [endpoint_agent_tamper_protection] is enabled. Nothing to do!' + ); + }); + }); + + describe('and `agentTamperProtection` is disabled', () => { + let policyGenerator: FleetAgentPolicyGenerator; + let page1Items: GetAgentPoliciesResponseItem[] = []; + let page2Items: GetAgentPoliciesResponseItem[] = []; + let page3Items: GetAgentPoliciesResponseItem[] = []; + let bulkUpdateResponse: AgentPolicy[]; + + const generatePolicyMock = (): GetAgentPoliciesResponseItem => { + return policyGenerator.generate({ is_protected: true }); + }; + + beforeEach(() => { + policyGenerator = new FleetAgentPolicyGenerator('seed'); + const agentPolicyListSrv = fleetServices.agentPolicy.list as jest.Mock; + + appFeatureService = createAppFeaturesServiceMock( + ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_agent_tamper_protection') + ); + + page1Items = [generatePolicyMock(), generatePolicyMock()]; + page2Items = [generatePolicyMock(), generatePolicyMock()]; + page3Items = [generatePolicyMock()]; + + agentPolicyListSrv + .mockImplementationOnce(async () => { + return { + total: 2500, + page: 1, + perPage: 1000, + items: page1Items, + }; + }) + .mockImplementationOnce(async () => { + return { + total: 2500, + page: 2, + perPage: 1000, + items: page2Items, + }; + }) + .mockImplementationOnce(async () => { + return { + total: 2500, + page: 3, + perPage: 1000, + items: page3Items, + }; + }); + + bulkUpdateResponse = [ + page1Items[0], + page1Items[1], + page2Items[0], + page2Items[1], + page3Items[0], + ]; + + (fleetServices.agentPolicy.bumpRevision as jest.Mock).mockImplementation(async () => { + return bulkUpdateResponse; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should update only policies that have protections turn on', async () => { + await callTurnOffAgentPolicyFeatures(); + + expect(fleetServices.agentPolicy.list as jest.Mock).toHaveBeenCalledTimes(3); + + const updates = Array.from({ length: 5 }, (_, i) => ({ + soClient: fleetServices.internalSoClient, + esClient, + id: bulkUpdateResponse![i].id, + })); + + expect(fleetServices.agentPolicy.bumpRevision as jest.Mock).toHaveBeenCalledTimes(5); + updates.forEach((args, i) => { + expect(fleetServices.agentPolicy.bumpRevision as jest.Mock).toHaveBeenNthCalledWith( + i + 1, + args.soClient, + args.esClient, + args.id, + { removeProtection: true, user: { username: 'elastic' } } + ); + }); + + expect(logger.info).toHaveBeenCalledWith( + 'App feature [endpoint_agent_tamper_protection] is disabled. Checking fleet agent policies for compliance' + ); + + expect(logger.info).toHaveBeenCalledWith( + `Found 5 policies that need updates:\n${bulkUpdateResponse! + .map( + (policy) => + `Policy [${policy.id}][${policy.name}] updated to disable agent tamper protection.` + ) + .join('\n')}` + ); + expect(logger.info).toHaveBeenCalledWith('Done. All updates applied successfully'); + }); + + it('should log failures', async () => { + (fleetServices.agentPolicy.bumpRevision as jest.Mock).mockImplementationOnce(async () => { + throw new Error('oh noo'); + }); + await callTurnOffAgentPolicyFeatures(); + + expect(logger.error).toHaveBeenCalledWith( + `Done - 1 out of 5 were successful. Errors encountered:\nPolicy [${ + bulkUpdateResponse![0].id + }] failed to update due to error: Error: oh noo` + ); + + expect(fleetServices.agentPolicy.bumpRevision as jest.Mock).toHaveBeenCalledTimes(5); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.ts new file mode 100644 index 0000000000000..0d1b01d68b765 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger, ElasticsearchClient } from '@kbn/core/server'; +import type { AgentPolicy } from '@kbn/fleet-plugin/common'; +import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import { AppFeatureSecurityKey } from '@kbn/security-solution-features/keys'; +import pMap from 'p-map'; +import type { EndpointInternalFleetServicesInterface } from '../services/fleet'; +import type { AppFeaturesService } from '../../lib/app_features_service/app_features_service'; + +export const turnOffAgentPolicyFeatures = async ( + esClient: ElasticsearchClient, + fleetServices: EndpointInternalFleetServicesInterface, + appFeaturesService: AppFeaturesService, + logger: Logger +): Promise => { + const log = logger.get('endpoint', 'agentPolicyFeatures'); + + if (appFeaturesService.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection)) { + log.info( + `App feature [${AppFeatureSecurityKey.endpointAgentTamperProtection}] is enabled. Nothing to do!` + ); + + return; + } + + log.info( + `App feature [${AppFeatureSecurityKey.endpointAgentTamperProtection}] is disabled. Checking fleet agent policies for compliance` + ); + + const { agentPolicy: agentPolicyService, internalSoClient } = fleetServices; + const updates: AgentPolicy[] = []; + const messages: string[] = []; + const perPage = 1000; + let hasMoreData = true; + let total = 0; + let page = 1; + + do { + const currentPage = page++; + const { items, total: totalPolicies } = await agentPolicyService.list(internalSoClient, { + page: currentPage, + kuery: 'ingest-agent-policies.is_protected: true', + perPage, + }); + + total = totalPolicies; + hasMoreData = currentPage * perPage < total; + + for (const item of items) { + messages.push( + `Policy [${item.id}][${item.name}] updated to disable agent tamper protection.` + ); + + updates.push({ ...item, is_protected: false }); + } + } while (hasMoreData); + + if (updates.length > 0) { + logger.info(`Found ${updates.length} policies that need updates:\n${messages.join('\n')}`); + const policyUpdateErrors: Array<{ id: string; error: Error }> = []; + await pMap(updates, async (update) => { + try { + return await agentPolicyService.bumpRevision(internalSoClient, esClient, update.id, { + user: { username: 'elastic' } as AuthenticatedUser, + removeProtection: true, + }); + } catch (error) { + policyUpdateErrors.push({ error, id: update.id }); + } + }); + + if (policyUpdateErrors.length > 0) { + logger.error( + `Done - ${policyUpdateErrors.length} out of ${ + updates.length + } were successful. Errors encountered:\n${policyUpdateErrors + .map((e) => `Policy [${e.id}] failed to update due to error: ${e.error}`) + .join('\n')}` + ); + } else { + logger.info(`Done. All updates applied successfully`); + } + } else { + logger.info(`Done. Checked ${total} policies and no updates needed`); + } +}; 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 b2c98e5fef9e7..f3700f9f0a70f 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 @@ -24,13 +24,15 @@ import { } from '../../common/endpoint/models/policy_config'; import { buildManifestManagerMock } from '../endpoint/services/artifacts/manifest_manager/manifest_manager.mock'; import { + getAgentPolicyCreateCallback, + getAgentPolicyUpdateCallback, getPackagePolicyCreateCallback, getPackagePolicyDeleteCallback, getPackagePolicyPostCreateCallback, getPackagePolicyUpdateCallback, } from './fleet_integration'; -import type { KibanaRequest } from '@kbn/core/server'; -import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-features/keys'; +import type { KibanaRequest, Logger } from '@kbn/core/server'; +import { ALL_APP_FEATURE_KEYS, AppFeatureSecurityKey } from '@kbn/security-solution-features/keys'; import { requestContextMock } from '../lib/detection_engine/routes/__mocks__'; import { requestContextFactoryMock } from '../request_context_factory.mock'; import type { EndpointAppContextServiceStartContract } from '../endpoint/endpoint_app_context_services'; @@ -50,7 +52,10 @@ import { getMockArtifacts, toArtifactRecords } from '../endpoint/lib/artifacts/m import { Manifest } from '../endpoint/lib/artifacts'; import type { NewPackagePolicy, PackagePolicy } from '@kbn/fleet-plugin/common/types/models'; import type { ManifestSchema } from '../../common/endpoint/schema/manifest'; -import type { PostDeletePackagePoliciesResponse } from '@kbn/fleet-plugin/common'; +import type { + GetAgentPoliciesResponseItem, + PostDeletePackagePoliciesResponse, +} from '@kbn/fleet-plugin/common'; 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'; @@ -58,6 +63,7 @@ import { disableProtections } from '../../common/endpoint/models/policy_config_h import type { AppFeaturesService } from '../lib/app_features_service/app_features_service'; import { createAppFeaturesServiceMock } from '../lib/app_features_service/mocks'; import * as moment from 'moment'; +import type { PostAgentPolicyCreateCallback } from '@kbn/fleet-plugin/server/types'; jest.mock('uuid', () => ({ v4: (): string => 'NEW_UUID', @@ -382,6 +388,115 @@ describe('ingest_integration tests ', () => { }); }); + describe('agent policy update callback', () => { + it('AppFeature disabled - returns an error if higher tier features are turned on in the policy', async () => { + const logger = loggingSystemMock.create().get('ingest_integration.test'); + + appFeaturesService = createAppFeaturesServiceMock( + ALL_APP_FEATURE_KEYS.filter( + (key) => key !== AppFeatureSecurityKey.endpointAgentTamperProtection + ) + ); + const callback = getAgentPolicyUpdateCallback(logger, appFeaturesService); + + const policyConfig = generator.generateAgentPolicy(); + policyConfig.is_protected = true; + + await expect(() => callback(policyConfig)).rejects.toThrow( + 'Agent Tamper Protection is not allowed in current environment' + ); + }); + it('AppFeature disabled - returns agent policy if higher tier features are turned off in the policy', async () => { + const logger = loggingSystemMock.create().get('ingest_integration.test'); + + appFeaturesService = createAppFeaturesServiceMock( + ALL_APP_FEATURE_KEYS.filter( + (key) => key !== AppFeatureSecurityKey.endpointAgentTamperProtection + ) + ); + const callback = getAgentPolicyUpdateCallback(logger, appFeaturesService); + + const policyConfig = generator.generateAgentPolicy(); + + const updatedPolicyConfig = await callback(policyConfig); + + expect(updatedPolicyConfig).toEqual(policyConfig); + }); + it('AppFeature enabled - returns agent policy if higher tier features are turned on in the policy', async () => { + const logger = loggingSystemMock.create().get('ingest_integration.test'); + + const callback = getAgentPolicyUpdateCallback(logger, appFeaturesService); + + const policyConfig = generator.generateAgentPolicy(); + policyConfig.is_protected = true; + + const updatedPolicyConfig = await callback(policyConfig); + + expect(updatedPolicyConfig).toEqual(policyConfig); + }); + it('AppFeature enabled - returns agent policy if higher tier features are turned off in the policy', async () => { + const logger = loggingSystemMock.create().get('ingest_integration.test'); + + const callback = getAgentPolicyUpdateCallback(logger, appFeaturesService); + const policyConfig = generator.generateAgentPolicy(); + + const updatedPolicyConfig = await callback(policyConfig); + + expect(updatedPolicyConfig).toEqual(policyConfig); + }); + }); + + describe('agent policy create callback', () => { + let logger: Logger; + let callback: PostAgentPolicyCreateCallback; + let policyConfig: GetAgentPoliciesResponseItem; + + beforeEach(() => { + logger = loggingSystemMock.create().get('ingest_integration.test'); + callback = getAgentPolicyCreateCallback(logger, appFeaturesService); + policyConfig = generator.generateAgentPolicy(); + }); + + it('AppFeature disabled - returns an error if higher tier features are turned on in the policy', async () => { + appFeaturesService = createAppFeaturesServiceMock( + ALL_APP_FEATURE_KEYS.filter( + (key) => key !== AppFeatureSecurityKey.endpointAgentTamperProtection + ) + ); + callback = getAgentPolicyCreateCallback(logger, appFeaturesService); + policyConfig.is_protected = true; + + await expect(() => callback(policyConfig)).rejects.toThrow( + 'Agent Tamper Protection is not allowed in current environment' + ); + }); + + it('AppFeature disabled - returns agent policy if higher tier features are turned off in the policy', async () => { + appFeaturesService = createAppFeaturesServiceMock( + ALL_APP_FEATURE_KEYS.filter( + (key) => key !== AppFeatureSecurityKey.endpointAgentTamperProtection + ) + ); + callback = getAgentPolicyCreateCallback(logger, appFeaturesService); + const updatedPolicyConfig = await callback(policyConfig); + + expect(updatedPolicyConfig).toEqual(policyConfig); + }); + + it('AppFeature enabled - returns agent policy if higher tier features are turned on in the policy', async () => { + policyConfig.is_protected = true; + const updatedPolicyConfig = await callback(policyConfig); + + expect(updatedPolicyConfig).toEqual(policyConfig); + }); + + it('AppFeature enabled - returns agent policy if higher tier features are turned off in the policy', async () => { + const updatedPolicyConfig = await callback(policyConfig); + + expect(updatedPolicyConfig).toEqual(policyConfig); + }); + }); + describe('package policy update callback (when the license is below platinum)', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; 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 b2e2ab65742e7..f01f2da532f8b 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 @@ -16,6 +16,8 @@ import type { } from '@kbn/fleet-plugin/server'; import type { + AgentPolicy, + NewAgentPolicy, NewPackagePolicy, PackagePolicy, UpdatePackagePolicy, @@ -23,6 +25,10 @@ import type { import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types'; import { AppFeatureSecurityKey } from '@kbn/security-solution-features/keys'; +import type { + PostAgentPolicyCreateCallback, + PostAgentPolicyUpdateCallback, +} from '@kbn/fleet-plugin/server/types'; import { validatePolicyAgainstAppFeatures } from './handlers/validate_policy_against_app_features'; import { validateEndpointPackagePolicy } from './handlers/validate_endpoint_package_policy'; import { @@ -291,6 +297,54 @@ export const getPackagePolicyPostCreateCallback = ( }; }; +const throwAgentTamperProtectionUnavailableError = ( + logger: Logger, + policyName?: string, + policyId?: string +): void => { + const agentTamperProtectionUnavailableError: Error & { + statusCode?: number; + apiPassThrough?: boolean; + } = new Error('Agent Tamper Protection is not allowed in current environment'); + // Agent Policy Service will check for apiPassThrough and rethrow. Route handler will check for statusCode and overwrite. + agentTamperProtectionUnavailableError.statusCode = 403; + agentTamperProtectionUnavailableError.apiPassThrough = true; + logger.error( + `Policy [${policyName}:${policyId}] error: Agent Tamper Protection requires Complete Endpoint Security tier` + ); + throw agentTamperProtectionUnavailableError; +}; + +export const getAgentPolicyCreateCallback = ( + logger: Logger, + appFeatures: AppFeaturesService +): PostAgentPolicyCreateCallback => { + return async (agentPolicy: NewAgentPolicy): Promise => { + if ( + agentPolicy.is_protected && + !appFeatures.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection) + ) { + throwAgentTamperProtectionUnavailableError(logger, agentPolicy.name, agentPolicy.id); + } + return agentPolicy; + }; +}; + +export const getAgentPolicyUpdateCallback = ( + logger: Logger, + appFeatures: AppFeaturesService +): PostAgentPolicyUpdateCallback => { + return async (agentPolicy: Partial): Promise> => { + if ( + agentPolicy.is_protected && + !appFeatures.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection) + ) { + throwAgentTamperProtectionUnavailableError(logger, agentPolicy.name, agentPolicy.id); + } + return agentPolicy; + }; +}; + export const getPackagePolicyDeleteCallback = ( exceptionsClient: ExceptionListClient | undefined, savedObjectsClient: SavedObjectsClientContract | undefined diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index ffe4e7a6e342b..aaa104533e080 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -115,6 +115,7 @@ import { } from '../common/entity_analytics/risk_engine'; import { isEndpointPackageV2 } from '../common/endpoint/utils/package_v2'; import { getAssistantTools } from './assistant/tools'; +import { turnOffAgentPolicyFeatures } from './endpoint/migrations/turn_off_agent_policy_features'; export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract'; @@ -555,6 +556,13 @@ export class Plugin implements ISecuritySolutionPlugin { appFeaturesService, logger ); + + turnOffAgentPolicyFeatures( + core.elasticsearch.client.asInternalUser, + endpointFleetServicesFactory.asInternalUser(), + appFeaturesService, + logger + ); }); // License related start