From cb3c9b24bfa75746c7cf855923693503aff73f48 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Fri, 5 Jan 2024 21:32:15 +0100 Subject: [PATCH 01/11] upsell on essential --- .../features/src/app_features_keys.ts | 5 + .../src/security/app_feature_config.ts | 2 +- x-pack/plugins/fleet/server/plugin.ts | 13 +- .../fleet/server/services/agent_policy.ts | 43 ++++++- .../fleet/server/services/app_context.ts | 22 +++- .../plugins/fleet/server/types/extensions.ts | 27 ++++- .../endpoint/endpoint_app_context_services.ts | 11 ++ .../turn_off_agent_tamper_protection.ts | 114 ++++++++++++++++++ .../fleet_integration.test.ts | 2 + .../fleet_integration/fleet_integration.ts | 40 ++++++ .../common/pli/pli_config.ts | 1 + 11 files changed, 270 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_tamper_protection.ts diff --git a/x-pack/packages/security-solution/features/src/app_features_keys.ts b/x-pack/packages/security-solution/features/src/app_features_keys.ts index ed8923cdb229a..99a2f83830dcc 100644 --- a/x-pack/packages/security-solution/features/src/app_features_keys.ts +++ b/x-pack/packages/security-solution/features/src/app_features_keys.ts @@ -44,6 +44,11 @@ export enum AppFeatureSecurityKey { */ osqueryAutomatedResponseActions = 'osquery_automated_response_actions', + /** + * Enables Agent Tamper Protection + */ + endpointAgentTamperProtection = 'endpoint_agent_tamper_protection', + /** * Enables managing endpoint exceptions on rules and alerts */ diff --git a/x-pack/packages/security-solution/features/src/security/app_feature_config.ts b/x-pack/packages/security-solution/features/src/security/app_feature_config.ts index 66bbfb4e5ddcd..7f6f3fc545b45 100644 --- a/x-pack/packages/security-solution/features/src/security/app_feature_config.ts +++ b/x-pack/packages/security-solution/features/src/security/app_feature_config.ts @@ -106,6 +106,6 @@ export const securityDefaultAppFeaturesConfig: DefaultSecurityAppFeaturesConfig }, [AppFeatureSecurityKey.osqueryAutomatedResponseActions]: {}, - + [AppFeatureSecurityKey.endpointAgentTamperProtection]: {}, [AppFeatureSecurityKey.externalRuleActions]: {}, }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 8333d0257a8f0..28367fbcffaa8 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -91,7 +91,11 @@ import { import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects'; import { registerRoutes } from './routes'; -import type { ExternalCallback, FleetRequestHandlerContext } from './types'; +import type { + ExternalCallback, + ExternalCallbackAgentPolicy, + FleetRequestHandlerContext, +} from './types'; import type { ESIndexPatternService, AgentService, @@ -218,7 +222,7 @@ export interface FleetStartContract { * Register callbacks for inclusion in fleet API processing * @param args */ - registerExternalCallback: (...args: ExternalCallback) => void; + registerExternalCallback: (...args: ExternalCallback | ExternalCallbackAgentPolicy) => void; /** * Create a Fleet Artifact Client instance @@ -626,7 +630,10 @@ export class FleetPlugin getByIds: agentPolicyService.getByIDs, }, packagePolicyService, - registerExternalCallback: (type: ExternalCallback[0], callback: ExternalCallback[1]) => { + registerExternalCallback: ( + type: ExternalCallback[0] | ExternalCallbackAgentPolicy[0], + callback: ExternalCallback[1] | ExternalCallbackAgentPolicy[1] + ) => { return appContextService.addExternalCallback(type, callback); }, createArtifactsClient(packageName: string) { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index b44e0616b7b69..31ba16a304106 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, + ExternalCallbackAgentPolicy, + PostAgentPolicyCreateCallback, + PostAgentPolicyUpdateCallback, } from '../types'; import { getAllowedOutputTypeForPolicy, @@ -231,6 +234,43 @@ class AgentPolicyService { return policyHasSyntheticsIntegration(agentPolicy); } + public async runExternalCallbacks( + externalCallbackType: ExternalCallbackAgentPolicy[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, @@ -251,7 +291,7 @@ class AgentPolicyService { id: options.id, savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE, }); - + await this.runExternalCallbacks('agentPolicyCreate', agentPolicy); this.checkTamperProtectionLicense(agentPolicy); const logger = appContextService.getLogger(); @@ -516,6 +556,7 @@ class AgentPolicyService { throw new AgentPolicyNotFoundError('Agent policy not found'); } + await this.runExternalCallbacks('agentPolicyUpdate', agentPolicy); 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..9137cb45dfa60 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -42,6 +42,9 @@ import type { PostPackagePolicyPostDeleteCallback, PostPackagePolicyPostCreateCallback, PutPackagePolicyUpdateCallback, + ExternalCallbackAgentPolicy, + PostAgentPolicyCreateCallback, + PostAgentPolicyUpdateCallback, } from '../types'; import type { FleetAppContext } from '../plugin'; import type { TelemetryEventsSender } from '../telemetry/sender'; @@ -234,18 +237,25 @@ class AppContextService { return this.kibanaInstanceId; } - public addExternalCallback(type: ExternalCallback[0], callback: ExternalCallback[1]) { + public addExternalCallback( + type: ExternalCallback[0] | ExternalCallbackAgentPolicy[0], + callback: ExternalCallback[1] | ExternalCallbackAgentPolicy[1] + ) { if (!this.externalCallbacks.has(type)) { this.externalCallbacks.set(type, new Set()); } this.externalCallbacks.get(type)!.add(callback); } - public getExternalCallbacks( + public getExternalCallbacks( type: T ): | Set< - T extends 'packagePolicyCreate' + T extends 'agentPolicyCreate' + ? PostAgentPolicyCreateCallback + : T extends 'agentPolicyUpdate' + ? PostAgentPolicyUpdateCallback + : T extends 'packagePolicyCreate' ? PostPackagePolicyCreateCallback : T extends 'packagePolicyDelete' ? PostPackagePolicyDeleteCallback @@ -258,7 +268,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/types/extensions.ts b/x-pack/plugins/fleet/server/types/extensions.ts index ca5a0d84c958e..db1c70f5d9976 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', @@ -81,4 +91,19 @@ export type ExternalCallback = | ExternalCallbackPostDelete | ExternalCallbackUpdate; -export type ExternalCallbacksStorage = Map>; +export type ExternalCallbackAgentPolicyCreate = [ + 'agentPolicyCreate', + PostAgentPolicyCreateCallback +]; +export type ExternalCallbackAgentPolicyUpdate = [ + 'agentPolicyUpdate', + PostAgentPolicyUpdateCallback +]; + +export type ExternalCallbackAgentPolicy = + | ExternalCallbackAgentPolicyCreate + | ExternalCallbackAgentPolicyUpdate; + +type ExternalCallbackType = ExternalCallback | ExternalCallbackAgentPolicy; + +export type ExternalCallbacksStorage = Map>; 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_tamper_protection.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_tamper_protection.ts new file mode 100644 index 0000000000000..122eda40aa834 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_tamper_protection.ts @@ -0,0 +1,114 @@ +/* + * 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. + */ + +// /* +// * 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +// import type { Logger } from '@kbn/logging'; +// import { AppFeatureSecurityKey } from '@kbn/security-solution-features/src/app_features_keys'; +// import type { AgentPolicy } from '@kbn/fleet-plugin/common'; +// import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; +// import type { EndpointInternalFleetServicesInterface } from '../services/fleet'; +// import type { AppFeaturesService } from '../../lib/app_features_service'; +// import { +// ensureOnlyEventCollectionIsAllowed, +// isPolicySetToEventCollectionOnly, +// } from '../../../common/endpoint/models/policy_config_helpers'; +// import { getPolicyDataForUpdate } from '../../../common/endpoint/service/policy'; +// +// export const turnOffAgentTamperProtectionIfNotSupported = async ( +// esClient: ElasticsearchClient, +// fleetServices: EndpointInternalFleetServicesInterface, +// appFeaturesService: AppFeaturesService, +// logger: Logger +// ): Promise => { +// const log = logger.get('endpoint', 'agentPolicy'); +// +// 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 endpoint integration policies for compliance` +// ); +// +// const { agentPolicy, 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 agentPolicy.list(internalSoClient, { +// page: currentPage, +// kuery: 'ingest-agent-policies.is_protected: true', +// perPage, +// }); +// +// total = totalPolicies; +// hasMoreData = currentPage * perPage < total; +// +// for (const item of items) { +// const integrationPolicy = item; +// 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/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 7c861bf87f18f..77ca2882262b0 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 @@ -382,6 +382,8 @@ describe('ingest_integration tests ', () => { }); }); + describe('agent policy update callback (when appFeature is not enabled)', () => {}); + 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 23da392b96eb0..8ea68de2b571b 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 { validateEndpointPackagePolicy } from './handlers/validate_endpoint_package_policy'; import { isPolicySetToEventCollectionOnly, @@ -286,6 +292,40 @@ export const getPackagePolicyPostCreateCallback = ( }; }; +export const getAgentPolicyCreateCallback = ( + logger: Logger, + appFeatures: AppFeaturesService +): PostAgentPolicyCreateCallback => { + return async (agentPolicy: NewAgentPolicy): Promise => { + if ( + agentPolicy.is_protected && + !appFeatures.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection) + ) { + const error = new Error('Agent Tamper Protection requires Complete Endpoint Security tier'); + logger.error(error); + throw error; + } + return agentPolicy; + }; +}; + +export const getAgentPolicyUpdateCallback = ( + logger: Logger, + appFeatures: AppFeaturesService +): PostAgentPolicyUpdateCallback => { + return async (agentPolicy: Partial): Promise> => { + if ( + agentPolicy.is_protected && + !appFeatures.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection) + ) { + const error = new Error('Agent Tamper Protection requires Complete Endpoint Security tier'); + logger.error(error); + throw error; + } + return agentPolicy; + }; +}; + export const getPackagePolicyDeleteCallback = ( exceptionsClient: ExceptionListClient | undefined, savedObjectsClient: SavedObjectsClientContract | undefined diff --git a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts index 3dcb800dc7a74..064197dedaf4d 100644 --- a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts +++ b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts @@ -34,6 +34,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = { complete: [ AppFeatureKey.endpointResponseActions, AppFeatureKey.osqueryAutomatedResponseActions, + AppFeatureKey.endpointAgentTamperProtection, AppFeatureKey.endpointExceptions, ], }, From 0c4e21a83e0ac71799cc1894f59c73aa146647e3 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Tue, 16 Jan 2024 16:08:45 +0100 Subject: [PATCH 02/11] upsell on essential --- x-pack/plugins/fleet/server/plugin.ts | 60 ++++--- .../services/agent_policy_watch.test.ts | 168 +++++++++++------- .../server/services/agent_policy_watch.ts | 98 ++++++++++ .../turn_off_agent_tamper_protection.ts | 114 ------------ .../fleet_integration.test.ts | 121 ++++++++++++- .../security_solution/server/plugin.ts | 6 + 6 files changed, 361 insertions(+), 206 deletions(-) delete mode 100644 x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_tamper_protection.ts diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 28367fbcffaa8..38ac5d2e3276c 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -8,33 +8,32 @@ import { backOff } from 'exponential-backoff'; import type { Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs'; -import { take, filter } from 'rxjs/operators'; +import { filter, take } from 'rxjs/operators'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { i18n } from '@kbn/i18n'; import type { CoreSetup, CoreStart, + ElasticsearchClient, ElasticsearchServiceStart, + HttpServiceSetup, + KibanaRequest, Logger, Plugin, PluginInitializerContext, + SavedObjectsClientContract, SavedObjectsServiceStart, - HttpServiceSetup, - KibanaRequest, ServiceStatus, - ElasticsearchClient, - SavedObjectsClientContract, } from '@kbn/core/server'; +import { DEFAULT_APP_CATEGORIES, SavedObjectsClient, ServiceStatusLevels } from '@kbn/core/server'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import type { TelemetryPluginSetup, TelemetryPluginStart } from '@kbn/telemetry-plugin/server'; - -import { DEFAULT_APP_CATEGORIES, SavedObjectsClient, ServiceStatusLevels } from '@kbn/core/server'; import type { PluginStart as DataPluginStart } from '@kbn/data-plugin/server'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/server'; import type { - EncryptedSavedObjectsPluginStart, EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '@kbn/encrypted-saved-objects-plugin/server'; import type { AuditLogger, @@ -55,40 +54,41 @@ import type { SavedObjectTaggingStart } from '@kbn/saved-objects-tagging-plugin/ import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; +import type { AppFeatureKeyType } from '@kbn/security-solution-features'; + import type { FleetConfigType } from '../common/types'; import type { FleetAuthz } from '../common'; -import type { ExperimentalFeatures } from '../common/experimental_features'; - import { - MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, INTEGRATIONS_PLUGIN_ID, + MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, } from '../common'; +import type { ExperimentalFeatures } from '../common/experimental_features'; import { parseExperimentalConfigValue } from '../common/experimental_features'; import { getFilesClientFactory } from './services/files/get_files_client_factory'; import type { MessageSigningServiceInterface } from './services/security'; import { - getRouteRequiredAuthz, - makeRouterWithFleetAuthz, calculateRouteAuthz, getAuthzFromRequest, + getRouteRequiredAuthz, + makeRouterWithFleetAuthz, MessageSigningService, } from './services/security'; import { - PLUGIN_ID, - OUTPUT_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE, - PACKAGE_POLICY_SAVED_OBJECT_TYPE, - PACKAGES_SAVED_OBJECT_TYPE, ASSETS_SAVED_OBJECT_TYPE, - PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, + OUTPUT_SAVED_OBJECT_TYPE, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + PACKAGES_SAVED_OBJECT_TYPE, + PLUGIN_ID, + PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, } from './constants'; -import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects'; +import { registerEncryptedSavedObjects, registerSavedObjects } from './saved_objects'; import { registerRoutes } from './routes'; import type { @@ -97,25 +97,25 @@ import type { FleetRequestHandlerContext, } from './types'; import type { - ESIndexPatternService, - AgentService, AgentPolicyServiceInterface, + AgentService, + ESIndexPatternService, PackageService, } from './services'; -import { FleetUsageSender } from './services'; import { + agentPolicyService, + AgentServiceImpl, appContextService, - licenseService, ESIndexPatternSavedObjectService, - agentPolicyService, + FleetUsageSender, + licenseService, packagePolicyService, - AgentServiceImpl, PackageServiceImpl, } from './services'; import { - registerFleetUsageCollector, fetchAgentsUsage, fetchFleetUsage, + registerFleetUsageCollector, } from './collectors/register'; import { FleetArtifactsClient } from './services/artifacts'; import type { FleetRouter } from './types/request_context'; @@ -245,6 +245,10 @@ export interface FleetStartContract { Function exported to allow creating unique ids for saved object tags */ getPackageSpecTagId: (spaceId: string, pkgName: string, tagName: string) => string; + appFeatureEnabled: ( + checkAppFeatureStatus: (appFeatureKey: AppFeatureKeyType) => boolean, + appFeatureKey: AppFeatureKeyType + ) => void; } export class FleetPlugin @@ -651,6 +655,10 @@ export class FleetPlugin return new FleetActionsClient(core.elasticsearch.client.asInternalUser, packageName); }, getPackageSpecTagId, + appFeatureEnabled: (checkAppFeatureStatus, appFeatureKey) => { + const isEnabled = checkAppFeatureStatus(appFeatureKey); + this.policyWatcher?.checkAppFeature(appFeatureKey, isEnabled); + }, }; } diff --git a/x-pack/plugins/fleet/server/services/agent_policy_watch.test.ts b/x-pack/plugins/fleet/server/services/agent_policy_watch.test.ts index 0f3e1d73cf5f5..3a75c1ff3cc65 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_watch.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_watch.test.ts @@ -14,6 +14,8 @@ import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { Subject } from 'rxjs'; +import { AppFeatureSecurityKey } from '@kbn/security-solution-features/src/app_features_keys'; + import { LicenseService } from '../../common/services'; import { createAgentPolicyMock } from '../../common/mocks'; @@ -25,90 +27,128 @@ jest.mock('./agent_policy'); const agentPolicySvcMock = agentPolicyService as jest.Mocked; -describe('Agent Policy-Changing license watcher', () => { +const createMockResponsePages = ( + total: number, + perPage: number, + overrideProps?: Record +) => { + let remainingTotal = total; + return Array.from({ length: Math.ceil(total / perPage) }, (_, index) => { + const itemsCount = Math.min(remainingTotal, perPage); + remainingTotal -= itemsCount; + return { + items: Array.from({ length: itemsCount }, () => createAgentPolicyMock(overrideProps)), + total, + page: index + 1, + perPage, + }; + }); +}; + +describe('Agent Policy Watch', () => { const logger = loggingSystemMock.create().get('license_watch.test'); const soStartMock = savedObjectsServiceMock.createStartContract(); const esStartMock = elasticsearchServiceMock.createStart(); - 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' } }); + afterEach(() => { + jest.clearAllMocks(); + }); - it('is activated on license changes', () => { - // mock a license-changing service to test reactivity - const licenseEmitter: Subject = new Subject(); - const licenseService = new LicenseService(); - const pw = new PolicyWatcher(soStartMock, esStartMock, logger); + describe('Agent Policy-Changing license watcher', () => { + 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' } }); - // swap out watch function, just to ensure it gets called when a license change happens - const mockWatch = jest.fn(); - pw.watch = mockWatch; + it('is activated on license changes', () => { + // mock a license-changing service to test reactivity + const licenseEmitter: Subject = new Subject(); + const licenseService = new LicenseService(); + const pw = new PolicyWatcher(soStartMock, esStartMock, logger); - // licenseService is watching our subject for incoming licenses - licenseService.start(licenseEmitter); - pw.start(licenseService); // and the PolicyWatcher under test, uses that to subscribe as well + // swap out watch function, just to ensure it gets called when a license change happens + const mockWatch = jest.fn(); + pw.watch = mockWatch; - // Enqueue a license change! - licenseEmitter.next(Platinum); + // licenseService is watching our subject for incoming licenses + licenseService.start(licenseEmitter); + pw.start(licenseService); // and the PolicyWatcher under test, uses that to subscribe as well - // policywatcher should have triggered - expect(mockWatch.mock.calls.length).toBe(1); + // Enqueue a license change! + licenseEmitter.next(Platinum); - pw.stop(); - licenseService.stop(); - licenseEmitter.complete(); - }); + // policywatcher should have triggered + expect(mockWatch.mock.calls.length).toBe(1); + + pw.stop(); + licenseService.stop(); + licenseEmitter.complete(); + }); + + it('pages through all agent policies', async () => { + const [firstPage, secondPage, thirdPage] = createMockResponsePages(247, 100); + + // set up the mocked agent policy service to return and do what we want + agentPolicySvcMock.list + .mockResolvedValueOnce(firstPage) + .mockResolvedValueOnce(secondPage) + .mockResolvedValueOnce(thirdPage); - it('pages through all agent policies', async () => { - const TOTAL = 247; - - // set up the mocked agent policy service to return and do what we want - agentPolicySvcMock.list - .mockResolvedValueOnce({ - items: Array.from({ length: 100 }, () => createAgentPolicyMock()), - total: TOTAL, - page: 1, - perPage: 100, - }) - .mockResolvedValueOnce({ - items: Array.from({ length: 100 }, () => createAgentPolicyMock()), - total: TOTAL, - page: 2, - perPage: 100, - }) - .mockResolvedValueOnce({ - items: Array.from({ length: TOTAL - 200 }, () => createAgentPolicyMock()), - total: TOTAL, - page: 3, - perPage: 100, + const pw = new PolicyWatcher(soStartMock, esStartMock, logger); + await pw.watch(Gold); // just manually trigger with a given license + + expect(agentPolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of results + + // Assert: on the first call to agentPolicy.list, we asked for page 1 + expect(agentPolicySvcMock.list.mock.calls[0][1].page).toBe(1); + expect(agentPolicySvcMock.list.mock.calls[1][1].page).toBe(2); // second call, asked for page 2 + expect(agentPolicySvcMock.list.mock.calls[2][1].page).toBe(3); // etc + }); + + it('alters no-longer-licensed features', async () => { + const [singlePage] = createMockResponsePages(1, 100, { + is_protected: true, }); + agentPolicySvcMock.list.mockResolvedValueOnce(singlePage); - const pw = new PolicyWatcher(soStartMock, esStartMock, logger); - await pw.watch(Gold); // just manually trigger with a given license + const pw = new PolicyWatcher(soStartMock, esStartMock, logger); - expect(agentPolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts + // emulate a license change below paid tier + await pw.watch(Basic); - // Assert: on the first call to agentPolicy.list, we asked for page 1 - expect(agentPolicySvcMock.list.mock.calls[0][1].page).toBe(1); - expect(agentPolicySvcMock.list.mock.calls[1][1].page).toBe(2); // second call, asked for page 2 - expect(agentPolicySvcMock.list.mock.calls[2][1].page).toBe(3); // etc + expect(agentPolicySvcMock.update).toHaveBeenCalled(); + expect(agentPolicySvcMock.update.mock.calls[0][3].is_protected).toEqual(false); + }); }); + describe('Agent Policy App Feature', () => { + it('does nothing if app feature is enabled', async () => { + const pw = new PolicyWatcher(soStartMock, esStartMock, logger); + await pw.checkAppFeature(AppFeatureSecurityKey.endpointAgentTamperProtection, true); - it('alters no-longer-licensed features', async () => { - // mock an agent policy with agent tamper protection enabled - agentPolicySvcMock.list.mockResolvedValueOnce({ - items: [createAgentPolicyMock({ is_protected: true })], - total: 1, - page: 1, - perPage: 100, + expect(agentPolicySvcMock.list.mock.calls.length).toBe(0); + expect(agentPolicySvcMock.bumpRevision.mock.calls.length).toBe(0); }); - const pw = new PolicyWatcher(soStartMock, esStartMock, logger); + it('alters agent policy if app feature is disabled', async () => { + const TOTAL = 2470; - // emulate a license change below paid tier - await pw.watch(Basic); + const [firstPage, secondPage, thirdPage] = createMockResponsePages(TOTAL, 1000, { + is_protected: true, + }); + + agentPolicySvcMock.list + .mockResolvedValueOnce(firstPage) + .mockResolvedValueOnce(secondPage) + .mockResolvedValueOnce(thirdPage); - expect(agentPolicySvcMock.update).toHaveBeenCalled(); - expect(agentPolicySvcMock.update.mock.calls[0][3].is_protected).toEqual(false); + const pw = new PolicyWatcher(soStartMock, esStartMock, logger); + await pw.checkAppFeature(AppFeatureSecurityKey.endpointAgentTamperProtection, false); + + expect(agentPolicySvcMock.list.mock.calls.length).toBe(3); + expect(agentPolicySvcMock.bumpRevision.mock.calls.length).toBe(TOTAL); + expect(agentPolicySvcMock.bumpRevision.mock.calls[0][3]).toHaveProperty( + 'removeProtection', + true + ); + }); }); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy_watch.ts b/x-pack/plugins/fleet/server/services/agent_policy_watch.ts index 1e10230a9acf8..d805b26d2a0c2 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_watch.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_watch.ts @@ -19,6 +19,14 @@ import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { pick } from 'lodash'; +import type { AppFeatureKeyType } from '@kbn/security-solution-features'; + +import { AppFeatureSecurityKey } from '@kbn/security-solution-features/src/app_features_keys'; + +import pMap from 'p-map'; + +import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; + import type { LicenseService } from '../../common/services/license'; import type { AgentPolicy } from '../../common'; @@ -64,6 +72,87 @@ export class PolicyWatcher { return soStart.getScopedClient(fakeRequest, { excludedExtensions: [SECURITY_EXTENSION_ID] }); } + private async checkAgentTamperProtectionAppFeature( + isEnabled: boolean, + appFeature: AppFeatureKeyType + ) { + const { logger } = this; + if (isEnabled) { + logger.info(`App feature [${appFeature}] is enabled. Nothing to do!`); + + return; + } + + logger.info( + `App feature [${appFeature}] is disabled. Checking endpoint integration policies for compliance` + ); + + const updates: AgentPolicy[] = []; + const messages: string[] = []; + const perPage = 1000; + let hasMoreData = true; + let total = 0; + let page = 1; + + logger.info(`Checking for policies with ${appFeature} enabled`); + + do { + const currentPage = page++; + const { items, total: totalPolicies } = await agentPolicyService.list( + this.makeInternalSOClient(this.soStart), + { + page: currentPage, + kuery: 'ingest-agent-policies.is_protected: false', + 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 }> = []; + const bulkUpdateResponse = await pMap(updates, async (update) => { + try { + return await agentPolicyService.bumpRevision( + this.makeInternalSOClient(this.soStart), + this.esClient, + update.id, + { + user: { username: 'elastic' } as AuthenticatedUser, + removeProtection: true, + } + ); + } catch (error) { + policyUpdateErrors.push({ error, id: update.id }); + } + }); + + logger.info(`Bulk update response:\n${JSON.stringify(bulkUpdateResponse, null, 2)}`); + + if (policyUpdateErrors.length > 0) { + logger.error(`Done. ${policyUpdateErrors.length} out of ${updates.length}`); + policyUpdateErrors.forEach((e) => + logger.error(`Policy [${e.id}] failed to update due to error: ${e.error}`) + ); + } else { + logger.info(`Done. All updates applied successfully`); + } + } else { + logger.info(`Done. Checked ${total} policies and no updates needed`); + } + } + public start(licenseService: LicenseService) { this.subscription = licenseService.getLicenseInformation$()?.subscribe(this.watch.bind(this)); } @@ -74,6 +163,15 @@ export class PolicyWatcher { } } + public checkAppFeature(appFeature: AppFeatureKeyType, isEnabled: boolean) { + switch (appFeature) { + case AppFeatureSecurityKey.endpointAgentTamperProtection: + return this.checkAgentTamperProtectionAppFeature(isEnabled, appFeature); + default: + return; + } + } + public async watch(license: ILicense) { let page = 1; let response: { diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_tamper_protection.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_tamper_protection.ts deleted file mode 100644 index 122eda40aa834..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_tamper_protection.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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. - */ - -// /* -// * 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -// import type { Logger } from '@kbn/logging'; -// import { AppFeatureSecurityKey } from '@kbn/security-solution-features/src/app_features_keys'; -// import type { AgentPolicy } from '@kbn/fleet-plugin/common'; -// import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; -// import type { EndpointInternalFleetServicesInterface } from '../services/fleet'; -// import type { AppFeaturesService } from '../../lib/app_features_service'; -// import { -// ensureOnlyEventCollectionIsAllowed, -// isPolicySetToEventCollectionOnly, -// } from '../../../common/endpoint/models/policy_config_helpers'; -// import { getPolicyDataForUpdate } from '../../../common/endpoint/service/policy'; -// -// export const turnOffAgentTamperProtectionIfNotSupported = async ( -// esClient: ElasticsearchClient, -// fleetServices: EndpointInternalFleetServicesInterface, -// appFeaturesService: AppFeaturesService, -// logger: Logger -// ): Promise => { -// const log = logger.get('endpoint', 'agentPolicy'); -// -// 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 endpoint integration policies for compliance` -// ); -// -// const { agentPolicy, 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 agentPolicy.list(internalSoClient, { -// page: currentPage, -// kuery: 'ingest-agent-policies.is_protected: true', -// perPage, -// }); -// -// total = totalPolicies; -// hasMoreData = currentPage * perPage < total; -// -// for (const item of items) { -// const integrationPolicy = item; -// 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/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 77ca2882262b0..3840f950f9dfa 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 { 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'; @@ -382,7 +384,122 @@ describe('ingest_integration tests ', () => { }); }); - describe('agent policy update callback (when appFeature is not enabled)', () => {}); + 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 requires Complete Endpoint Security tier' + ); + }); + 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', () => { + 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 = getAgentPolicyCreateCallback(logger, appFeaturesService); + + const policyConfig = generator.generateAgentPolicy(); + policyConfig.is_protected = true; + + await expect(() => callback(policyConfig)).rejects.toThrow( + 'Agent Tamper Protection requires Complete Endpoint Security tier' + ); + }); + + 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 = getAgentPolicyCreateCallback(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 = getAgentPolicyCreateCallback(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 = getAgentPolicyCreateCallback(logger, appFeaturesService); + const policyConfig = generator.generateAgentPolicy(); + + const updatedPolicyConfig = await callback(policyConfig); + + expect(updatedPolicyConfig).toEqual(policyConfig); + }); + }); describe('package policy update callback (when the license is below platinum)', () => { const soClient = savedObjectsClientMock.create(); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index ffe4e7a6e342b..4d46edbd17bd9 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -19,6 +19,7 @@ import type { ILicense } from '@kbn/licensing-plugin/server'; import { FLEET_ENDPOINT_PACKAGE } from '@kbn/fleet-plugin/common'; import { i18n } from '@kbn/i18n'; +import { AppFeatureSecurityKey } from '@kbn/security-solution-features/src/app_features_keys'; import { endpointPackagePoliciesStatsSearchStrategyProvider } from './search_strategy/endpoint_package_policies_stats'; import { turnOffPolicyProtectionsIfNotSupported } from './endpoint/migrations/turn_off_policy_protections'; import { endpointSearchStrategyProvider } from './search_strategy/endpoint'; @@ -555,6 +556,11 @@ export class Plugin implements ISecuritySolutionPlugin { appFeaturesService, logger ); + + plugins.fleet?.appFeatureEnabled( + appFeaturesService.isEnabled.bind(appFeaturesService), + AppFeatureSecurityKey.endpointAgentTamperProtection + ); }); // License related start From 65ad15e6b172dafb7f97a2e3e839462ba6e5c59f Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 16 Jan 2024 16:11:15 +0000 Subject: [PATCH 03/11] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/fleet/tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index 5b16316d9baaa..a8659b083055c 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -104,5 +104,7 @@ "@kbn/config", "@kbn/core-http-server-mocks", "@kbn/code-editor", + "@kbn/security-solution-features", + "@kbn/security-plugin-types-common", ] } From 77666a8dfac6779d45f28724374758a40bfcbeaf Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Thu, 25 Jan 2024 14:39:15 +0100 Subject: [PATCH 04/11] upsell on essential --- .../cloud_defend/server/plugin.test.ts | 9 +- .../server/plugin.test.ts | 4 +- x-pack/plugins/fleet/server/mocks/index.ts | 1 + x-pack/plugins/fleet/server/plugin.ts | 11 +- .../services/agent_policy_watch.test.ts | 168 +++++++----------- x-pack/plugins/fleet/server/services/index.ts | 1 + .../plugins/fleet/server/types/extensions.ts | 2 +- .../turn_off_agent_policy_features.test.ts | 166 +++++++++++++++++ .../turn_off_agent_policy_features.ts | 89 ++++++++++ .../security_solution/server/plugin.ts | 10 +- 10 files changed, 334 insertions(+), 127 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.ts diff --git a/x-pack/plugins/cloud_defend/server/plugin.test.ts b/x-pack/plugins/cloud_defend/server/plugin.test.ts index 2822385cd49e4..59c2943292979 100644 --- a/x-pack/plugins/cloud_defend/server/plugin.test.ts +++ b/x-pack/plugins/cloud_defend/server/plugin.test.ts @@ -25,11 +25,7 @@ import { CloudDefendPlugin } from './plugin'; import { CloudDefendPluginStartDeps } from './types'; import { createFleetAuthzMock } from '@kbn/fleet-plugin/common/mocks'; import { PackagePolicy, UpdatePackagePolicy } from '@kbn/fleet-plugin/common'; -import { - ExternalCallback, - FleetStartContract, - PostPackagePolicyPostCreateCallback, -} from '@kbn/fleet-plugin/server'; +import { FleetStartContract, PostPackagePolicyPostCreateCallback } from '@kbn/fleet-plugin/server'; import { INTEGRATION_PACKAGE_NAME } from '../common/constants'; import Chance from 'chance'; import type { AwaitedProperties } from '@kbn/utility-types'; @@ -42,6 +38,7 @@ import { import { securityMock } from '@kbn/security-plugin/server/mocks'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import * as onPackagePolicyPostCreateCallback from './lib/fleet_util'; +import { ExternalCallbackType } from '@kbn/fleet-plugin/server/types'; const chance = new Chance(); @@ -63,7 +60,7 @@ const createMockFleetStartContract = (): DeeplyMockedKeys => // @ts-expect-error 2322 packageService: createMockPackageService(), agentPolicyService: createMockAgentPolicyService(), - registerExternalCallback: jest.fn((..._: ExternalCallback) => {}), + registerExternalCallback: jest.fn((..._: ExternalCallbackType) => {}), packagePolicyService: createPackagePolicyServiceMock(), createArtifactsClient: jest.fn().mockReturnValue(createArtifactsClientMock()), }; 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..b5497e4e72b18 100644 --- a/x-pack/plugins/cloud_security_posture/server/plugin.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.test.ts @@ -32,7 +32,6 @@ import { UpdatePackagePolicy, } from '@kbn/fleet-plugin/common'; import { - ExternalCallback, FleetStartContract, PostPackagePolicyPostDeleteCallback, PostPackagePolicyPostCreateCallback, @@ -49,6 +48,7 @@ import { import { securityMock } from '@kbn/security-plugin/server/mocks'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import * as onPackagePolicyPostCreateCallback from './fleet_integration/fleet_integration'; +import { ExternalCallbackType } from '@kbn/fleet-plugin/server/types'; const chance = new Chance(); @@ -70,7 +70,7 @@ const createMockFleetStartContract = (): DeeplyMockedKeys => // @ts-expect-error 2322 packageService: createMockPackageService(), agentPolicyService: createMockAgentPolicyService(), - registerExternalCallback: jest.fn((..._: ExternalCallback) => {}), + registerExternalCallback: jest.fn((..._: ExternalCallbackType) => {}), packagePolicyService: createPackagePolicyServiceMock(), createArtifactsClient: jest.fn().mockReturnValue(createArtifactsClientMock()), }; 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 string; - appFeatureEnabled: ( - checkAppFeatureStatus: (appFeatureKey: AppFeatureKeyType) => boolean, - appFeatureKey: AppFeatureKeyType - ) => void; } export class FleetPlugin @@ -632,6 +626,7 @@ export class FleetPlugin list: agentPolicyService.list, getFullAgentPolicy: agentPolicyService.getFullAgentPolicy, getByIds: agentPolicyService.getByIDs, + bumpRevision: agentPolicyService.bumpRevision.bind(agentPolicyService), }, packagePolicyService, registerExternalCallback: ( @@ -655,10 +650,6 @@ export class FleetPlugin return new FleetActionsClient(core.elasticsearch.client.asInternalUser, packageName); }, getPackageSpecTagId, - appFeatureEnabled: (checkAppFeatureStatus, appFeatureKey) => { - const isEnabled = checkAppFeatureStatus(appFeatureKey); - this.policyWatcher?.checkAppFeature(appFeatureKey, isEnabled); - }, }; } diff --git a/x-pack/plugins/fleet/server/services/agent_policy_watch.test.ts b/x-pack/plugins/fleet/server/services/agent_policy_watch.test.ts index 3a75c1ff3cc65..0f3e1d73cf5f5 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_watch.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_watch.test.ts @@ -14,8 +14,6 @@ import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { Subject } from 'rxjs'; -import { AppFeatureSecurityKey } from '@kbn/security-solution-features/src/app_features_keys'; - import { LicenseService } from '../../common/services'; import { createAgentPolicyMock } from '../../common/mocks'; @@ -27,128 +25,90 @@ jest.mock('./agent_policy'); const agentPolicySvcMock = agentPolicyService as jest.Mocked; -const createMockResponsePages = ( - total: number, - perPage: number, - overrideProps?: Record -) => { - let remainingTotal = total; - return Array.from({ length: Math.ceil(total / perPage) }, (_, index) => { - const itemsCount = Math.min(remainingTotal, perPage); - remainingTotal -= itemsCount; - return { - items: Array.from({ length: itemsCount }, () => createAgentPolicyMock(overrideProps)), - total, - page: index + 1, - perPage, - }; - }); -}; - -describe('Agent Policy Watch', () => { +describe('Agent Policy-Changing license watcher', () => { const logger = loggingSystemMock.create().get('license_watch.test'); const soStartMock = savedObjectsServiceMock.createStartContract(); const esStartMock = elasticsearchServiceMock.createStart(); - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('Agent Policy-Changing license watcher', () => { - 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' } }); - - it('is activated on license changes', () => { - // mock a license-changing service to test reactivity - const licenseEmitter: Subject = new Subject(); - const licenseService = new LicenseService(); - const pw = new PolicyWatcher(soStartMock, esStartMock, logger); - - // swap out watch function, just to ensure it gets called when a license change happens - const mockWatch = jest.fn(); - pw.watch = mockWatch; - - // licenseService is watching our subject for incoming licenses - licenseService.start(licenseEmitter); - pw.start(licenseService); // and the PolicyWatcher under test, uses that to subscribe as well - - // Enqueue a license change! - licenseEmitter.next(Platinum); + 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' } }); - // policywatcher should have triggered - expect(mockWatch.mock.calls.length).toBe(1); + it('is activated on license changes', () => { + // mock a license-changing service to test reactivity + const licenseEmitter: Subject = new Subject(); + const licenseService = new LicenseService(); + const pw = new PolicyWatcher(soStartMock, esStartMock, logger); - pw.stop(); - licenseService.stop(); - licenseEmitter.complete(); - }); - - it('pages through all agent policies', async () => { - const [firstPage, secondPage, thirdPage] = createMockResponsePages(247, 100); + // swap out watch function, just to ensure it gets called when a license change happens + const mockWatch = jest.fn(); + pw.watch = mockWatch; - // set up the mocked agent policy service to return and do what we want - agentPolicySvcMock.list - .mockResolvedValueOnce(firstPage) - .mockResolvedValueOnce(secondPage) - .mockResolvedValueOnce(thirdPage); + // licenseService is watching our subject for incoming licenses + licenseService.start(licenseEmitter); + pw.start(licenseService); // and the PolicyWatcher under test, uses that to subscribe as well - const pw = new PolicyWatcher(soStartMock, esStartMock, logger); - await pw.watch(Gold); // just manually trigger with a given license + // Enqueue a license change! + licenseEmitter.next(Platinum); - expect(agentPolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of results + // policywatcher should have triggered + expect(mockWatch.mock.calls.length).toBe(1); - // Assert: on the first call to agentPolicy.list, we asked for page 1 - expect(agentPolicySvcMock.list.mock.calls[0][1].page).toBe(1); - expect(agentPolicySvcMock.list.mock.calls[1][1].page).toBe(2); // second call, asked for page 2 - expect(agentPolicySvcMock.list.mock.calls[2][1].page).toBe(3); // etc - }); + pw.stop(); + licenseService.stop(); + licenseEmitter.complete(); + }); - it('alters no-longer-licensed features', async () => { - const [singlePage] = createMockResponsePages(1, 100, { - is_protected: true, + it('pages through all agent policies', async () => { + const TOTAL = 247; + + // set up the mocked agent policy service to return and do what we want + agentPolicySvcMock.list + .mockResolvedValueOnce({ + items: Array.from({ length: 100 }, () => createAgentPolicyMock()), + total: TOTAL, + page: 1, + perPage: 100, + }) + .mockResolvedValueOnce({ + items: Array.from({ length: 100 }, () => createAgentPolicyMock()), + total: TOTAL, + page: 2, + perPage: 100, + }) + .mockResolvedValueOnce({ + items: Array.from({ length: TOTAL - 200 }, () => createAgentPolicyMock()), + total: TOTAL, + page: 3, + perPage: 100, }); - agentPolicySvcMock.list.mockResolvedValueOnce(singlePage); - const pw = new PolicyWatcher(soStartMock, esStartMock, logger); + const pw = new PolicyWatcher(soStartMock, esStartMock, logger); + await pw.watch(Gold); // just manually trigger with a given license - // emulate a license change below paid tier - await pw.watch(Basic); + expect(agentPolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts - expect(agentPolicySvcMock.update).toHaveBeenCalled(); - expect(agentPolicySvcMock.update.mock.calls[0][3].is_protected).toEqual(false); - }); + // Assert: on the first call to agentPolicy.list, we asked for page 1 + expect(agentPolicySvcMock.list.mock.calls[0][1].page).toBe(1); + expect(agentPolicySvcMock.list.mock.calls[1][1].page).toBe(2); // second call, asked for page 2 + expect(agentPolicySvcMock.list.mock.calls[2][1].page).toBe(3); // etc }); - describe('Agent Policy App Feature', () => { - it('does nothing if app feature is enabled', async () => { - const pw = new PolicyWatcher(soStartMock, esStartMock, logger); - await pw.checkAppFeature(AppFeatureSecurityKey.endpointAgentTamperProtection, true); - expect(agentPolicySvcMock.list.mock.calls.length).toBe(0); - expect(agentPolicySvcMock.bumpRevision.mock.calls.length).toBe(0); + it('alters no-longer-licensed features', async () => { + // mock an agent policy with agent tamper protection enabled + agentPolicySvcMock.list.mockResolvedValueOnce({ + items: [createAgentPolicyMock({ is_protected: true })], + total: 1, + page: 1, + perPage: 100, }); - it('alters agent policy if app feature is disabled', async () => { - const TOTAL = 2470; + const pw = new PolicyWatcher(soStartMock, esStartMock, logger); - const [firstPage, secondPage, thirdPage] = createMockResponsePages(TOTAL, 1000, { - is_protected: true, - }); - - agentPolicySvcMock.list - .mockResolvedValueOnce(firstPage) - .mockResolvedValueOnce(secondPage) - .mockResolvedValueOnce(thirdPage); + // emulate a license change below paid tier + await pw.watch(Basic); - const pw = new PolicyWatcher(soStartMock, esStartMock, logger); - await pw.checkAppFeature(AppFeatureSecurityKey.endpointAgentTamperProtection, false); - - expect(agentPolicySvcMock.list.mock.calls.length).toBe(3); - expect(agentPolicySvcMock.bumpRevision.mock.calls.length).toBe(TOTAL); - expect(agentPolicySvcMock.bumpRevision.mock.calls[0][3]).toHaveProperty( - 'removeProtection', - true - ); - }); + expect(agentPolicySvcMock.update).toHaveBeenCalled(); + expect(agentPolicySvcMock.update.mock.calls[0][3].is_protected).toEqual(false); }); }); 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/types/extensions.ts b/x-pack/plugins/fleet/server/types/extensions.ts index db1c70f5d9976..2b329c0cbc0e7 100644 --- a/x-pack/plugins/fleet/server/types/extensions.ts +++ b/x-pack/plugins/fleet/server/types/extensions.ts @@ -104,6 +104,6 @@ export type ExternalCallbackAgentPolicy = | ExternalCallbackAgentPolicyCreate | ExternalCallbackAgentPolicyUpdate; -type ExternalCallbackType = ExternalCallback | ExternalCallbackAgentPolicy; +export type ExternalCallbackType = ExternalCallback | ExternalCallbackAgentPolicy; export type ExternalCallbacksStorage = Map>; 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..1d1ab48792ea8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.test.ts @@ -0,0 +1,166 @@ +/* + * 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 endpoint integration 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`); + expect(logger.error).toHaveBeenCalledWith( + `Policy [${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..0ab8915a7d385 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.ts @@ -0,0 +1,89 @@ +/* + * 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 endpoint integration 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}`); + policyUpdateErrors.forEach((e) => + logger.error(`Policy [${e.id}] failed to update due to error: ${e.error}`) + ); + } 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/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 4d46edbd17bd9..aaa104533e080 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -19,7 +19,6 @@ import type { ILicense } from '@kbn/licensing-plugin/server'; import { FLEET_ENDPOINT_PACKAGE } from '@kbn/fleet-plugin/common'; import { i18n } from '@kbn/i18n'; -import { AppFeatureSecurityKey } from '@kbn/security-solution-features/src/app_features_keys'; import { endpointPackagePoliciesStatsSearchStrategyProvider } from './search_strategy/endpoint_package_policies_stats'; import { turnOffPolicyProtectionsIfNotSupported } from './endpoint/migrations/turn_off_policy_protections'; import { endpointSearchStrategyProvider } from './search_strategy/endpoint'; @@ -116,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'; @@ -557,9 +557,11 @@ export class Plugin implements ISecuritySolutionPlugin { logger ); - plugins.fleet?.appFeatureEnabled( - appFeaturesService.isEnabled.bind(appFeaturesService), - AppFeatureSecurityKey.endpointAgentTamperProtection + turnOffAgentPolicyFeatures( + core.elasticsearch.client.asInternalUser, + endpointFleetServicesFactory.asInternalUser(), + appFeaturesService, + logger ); }); From 30f54d0ebd53cd903a2ec1e778dcf2d12e51633f Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Thu, 25 Jan 2024 16:30:02 +0100 Subject: [PATCH 05/11] upsell on essential --- x-pack/plugins/fleet/server/services/preconfiguration.test.ts | 1 + 1 file changed, 1 insertion(+) 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(), }, })); From fa11c4b41e390c7bc84f844af4c86289cd30425d Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Fri, 26 Jan 2024 11:31:02 +0100 Subject: [PATCH 06/11] upsell on essential --- .../server/services/agent_policy_watch.ts | 98 ------------------- 1 file changed, 98 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agent_policy_watch.ts b/x-pack/plugins/fleet/server/services/agent_policy_watch.ts index d805b26d2a0c2..1e10230a9acf8 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_watch.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_watch.ts @@ -19,14 +19,6 @@ import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { pick } from 'lodash'; -import type { AppFeatureKeyType } from '@kbn/security-solution-features'; - -import { AppFeatureSecurityKey } from '@kbn/security-solution-features/src/app_features_keys'; - -import pMap from 'p-map'; - -import type { AuthenticatedUser } from '@kbn/security-plugin-types-common'; - import type { LicenseService } from '../../common/services/license'; import type { AgentPolicy } from '../../common'; @@ -72,87 +64,6 @@ export class PolicyWatcher { return soStart.getScopedClient(fakeRequest, { excludedExtensions: [SECURITY_EXTENSION_ID] }); } - private async checkAgentTamperProtectionAppFeature( - isEnabled: boolean, - appFeature: AppFeatureKeyType - ) { - const { logger } = this; - if (isEnabled) { - logger.info(`App feature [${appFeature}] is enabled. Nothing to do!`); - - return; - } - - logger.info( - `App feature [${appFeature}] is disabled. Checking endpoint integration policies for compliance` - ); - - const updates: AgentPolicy[] = []; - const messages: string[] = []; - const perPage = 1000; - let hasMoreData = true; - let total = 0; - let page = 1; - - logger.info(`Checking for policies with ${appFeature} enabled`); - - do { - const currentPage = page++; - const { items, total: totalPolicies } = await agentPolicyService.list( - this.makeInternalSOClient(this.soStart), - { - page: currentPage, - kuery: 'ingest-agent-policies.is_protected: false', - 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 }> = []; - const bulkUpdateResponse = await pMap(updates, async (update) => { - try { - return await agentPolicyService.bumpRevision( - this.makeInternalSOClient(this.soStart), - this.esClient, - update.id, - { - user: { username: 'elastic' } as AuthenticatedUser, - removeProtection: true, - } - ); - } catch (error) { - policyUpdateErrors.push({ error, id: update.id }); - } - }); - - logger.info(`Bulk update response:\n${JSON.stringify(bulkUpdateResponse, null, 2)}`); - - if (policyUpdateErrors.length > 0) { - logger.error(`Done. ${policyUpdateErrors.length} out of ${updates.length}`); - policyUpdateErrors.forEach((e) => - logger.error(`Policy [${e.id}] failed to update due to error: ${e.error}`) - ); - } else { - logger.info(`Done. All updates applied successfully`); - } - } else { - logger.info(`Done. Checked ${total} policies and no updates needed`); - } - } - public start(licenseService: LicenseService) { this.subscription = licenseService.getLicenseInformation$()?.subscribe(this.watch.bind(this)); } @@ -163,15 +74,6 @@ export class PolicyWatcher { } } - public checkAppFeature(appFeature: AppFeatureKeyType, isEnabled: boolean) { - switch (appFeature) { - case AppFeatureSecurityKey.endpointAgentTamperProtection: - return this.checkAgentTamperProtectionAppFeature(isEnabled, appFeature); - default: - return; - } - } - public async watch(license: ILicense) { let page = 1; let response: { From b18a7b4930a0c6885668642f493caba2006f8ee5 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 30 Jan 2024 09:43:39 +0000 Subject: [PATCH 07/11] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/fleet/tsconfig.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index a8659b083055c..5b16316d9baaa 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -104,7 +104,5 @@ "@kbn/config", "@kbn/core-http-server-mocks", "@kbn/code-editor", - "@kbn/security-solution-features", - "@kbn/security-plugin-types-common", ] } From 63b86df054c1c608c82f922033786bdc5254755b Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Wed, 31 Jan 2024 14:08:49 +0100 Subject: [PATCH 08/11] cr changes --- .../cloud_defend/server/plugin.test.ts | 9 ++- .../server/plugin.test.ts | 4 +- x-pack/plugins/fleet/server/plugin.ts | 13 +--- .../server/routes/agent_policy/handlers.ts | 12 +++ .../fleet/server/services/agent_policy.ts | 14 +++- .../fleet/server/services/app_context.ts | 8 +- .../plugins/fleet/server/types/extensions.ts | 24 +++--- .../api/agent_policy_settings_complete.cy.ts | 66 ++++++++++++++++ .../agent_policy_settings_essentials.cy.ts | 76 +++++++++++++++++++ .../public/management/cypress/tasks/fleet.ts | 27 ++++++- .../turn_off_agent_policy_features.ts | 11 ++- .../fleet_integration.test.ts | 48 ++++++------ .../fleet_integration/fleet_integration.ts | 20 +++-- 13 files changed, 256 insertions(+), 76 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/api/agent_policy_settings_complete.cy.ts create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/feature_access/api/agent_policy_settings_essentials.cy.ts diff --git a/x-pack/plugins/cloud_defend/server/plugin.test.ts b/x-pack/plugins/cloud_defend/server/plugin.test.ts index 59c2943292979..2822385cd49e4 100644 --- a/x-pack/plugins/cloud_defend/server/plugin.test.ts +++ b/x-pack/plugins/cloud_defend/server/plugin.test.ts @@ -25,7 +25,11 @@ import { CloudDefendPlugin } from './plugin'; import { CloudDefendPluginStartDeps } from './types'; import { createFleetAuthzMock } from '@kbn/fleet-plugin/common/mocks'; import { PackagePolicy, UpdatePackagePolicy } from '@kbn/fleet-plugin/common'; -import { FleetStartContract, PostPackagePolicyPostCreateCallback } from '@kbn/fleet-plugin/server'; +import { + ExternalCallback, + FleetStartContract, + PostPackagePolicyPostCreateCallback, +} from '@kbn/fleet-plugin/server'; import { INTEGRATION_PACKAGE_NAME } from '../common/constants'; import Chance from 'chance'; import type { AwaitedProperties } from '@kbn/utility-types'; @@ -38,7 +42,6 @@ import { import { securityMock } from '@kbn/security-plugin/server/mocks'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import * as onPackagePolicyPostCreateCallback from './lib/fleet_util'; -import { ExternalCallbackType } from '@kbn/fleet-plugin/server/types'; const chance = new Chance(); @@ -60,7 +63,7 @@ const createMockFleetStartContract = (): DeeplyMockedKeys => // @ts-expect-error 2322 packageService: createMockPackageService(), agentPolicyService: createMockAgentPolicyService(), - registerExternalCallback: jest.fn((..._: ExternalCallbackType) => {}), + registerExternalCallback: jest.fn((..._: ExternalCallback) => {}), packagePolicyService: createPackagePolicyServiceMock(), createArtifactsClient: jest.fn().mockReturnValue(createArtifactsClientMock()), }; 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 b5497e4e72b18..10edb5859ddee 100644 --- a/x-pack/plugins/cloud_security_posture/server/plugin.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.test.ts @@ -35,6 +35,7 @@ import { FleetStartContract, PostPackagePolicyPostDeleteCallback, PostPackagePolicyPostCreateCallback, + ExternalCallback, } from '@kbn/fleet-plugin/server'; import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../common/constants'; import Chance from 'chance'; @@ -48,7 +49,6 @@ import { import { securityMock } from '@kbn/security-plugin/server/mocks'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import * as onPackagePolicyPostCreateCallback from './fleet_integration/fleet_integration'; -import { ExternalCallbackType } from '@kbn/fleet-plugin/server/types'; const chance = new Chance(); @@ -70,7 +70,7 @@ const createMockFleetStartContract = (): DeeplyMockedKeys => // @ts-expect-error 2322 packageService: createMockPackageService(), agentPolicyService: createMockAgentPolicyService(), - registerExternalCallback: jest.fn((..._: ExternalCallbackType) => {}), + registerExternalCallback: jest.fn((..._: ExternalCallback) => {}), packagePolicyService: createPackagePolicyServiceMock(), createArtifactsClient: jest.fn().mockReturnValue(createArtifactsClientMock()), }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 1875f4ee788fa..046f7b24388ff 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -89,11 +89,7 @@ import { import { registerEncryptedSavedObjects, registerSavedObjects } from './saved_objects'; import { registerRoutes } from './routes'; -import type { - ExternalCallback, - ExternalCallbackAgentPolicy, - FleetRequestHandlerContext, -} from './types'; +import type { ExternalCallback, FleetRequestHandlerContext } from './types'; import type { AgentPolicyServiceInterface, AgentService, @@ -220,7 +216,7 @@ export interface FleetStartContract { * Register callbacks for inclusion in fleet API processing * @param args */ - registerExternalCallback: (...args: ExternalCallback | ExternalCallbackAgentPolicy) => void; + registerExternalCallback: (...args: ExternalCallback) => void; /** * Create a Fleet Artifact Client instance @@ -629,10 +625,7 @@ export class FleetPlugin bumpRevision: agentPolicyService.bumpRevision.bind(agentPolicyService), }, packagePolicyService, - registerExternalCallback: ( - type: ExternalCallback[0] | ExternalCallbackAgentPolicy[0], - callback: ExternalCallback[1] | ExternalCallbackAgentPolicy[1] - ) => { + registerExternalCallback: (type: ExternalCallback[0], callback: ExternalCallback[1]) => { return appContextService.addExternalCallback(type, callback); }, createArtifactsClient(packageName: string) { 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 27533f57c48d3..cb7b91ca8e9db 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -44,9 +44,9 @@ import type { FullAgentPolicy, ListWithKuery, NewPackagePolicy, - ExternalCallbackAgentPolicy, PostAgentPolicyCreateCallback, PostAgentPolicyUpdateCallback, + ExternalCallback, } from '../types'; import { getAllowedOutputTypeForPolicy, @@ -238,7 +238,7 @@ class AgentPolicyService { } public async runExternalCallbacks( - externalCallbackType: ExternalCallbackAgentPolicy[0], + externalCallbackType: ExternalCallback[0], agentPolicy: NewAgentPolicy | Partial ): Promise> { const logger = appContextService.getLogger(); @@ -559,8 +559,14 @@ class AgentPolicyService { if (!existingAgentPolicy) { throw new AgentPolicyNotFoundError('Agent policy not found'); } - - await this.runExternalCallbacks('agentPolicyUpdate', agentPolicy); + 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 9137cb45dfa60..65a0fb6c08f35 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -42,7 +42,6 @@ import type { PostPackagePolicyPostDeleteCallback, PostPackagePolicyPostCreateCallback, PutPackagePolicyUpdateCallback, - ExternalCallbackAgentPolicy, PostAgentPolicyCreateCallback, PostAgentPolicyUpdateCallback, } from '../types'; @@ -237,17 +236,14 @@ class AppContextService { return this.kibanaInstanceId; } - public addExternalCallback( - type: ExternalCallback[0] | ExternalCallbackAgentPolicy[0], - callback: ExternalCallback[1] | ExternalCallbackAgentPolicy[1] - ) { + public addExternalCallback(type: ExternalCallback[0], callback: ExternalCallback[1]) { if (!this.externalCallbacks.has(type)) { this.externalCallbacks.set(type, new Set()); } this.externalCallbacks.get(type)!.add(callback); } - public getExternalCallbacks( + public getExternalCallbacks( type: T ): | Set< diff --git a/x-pack/plugins/fleet/server/types/extensions.ts b/x-pack/plugins/fleet/server/types/extensions.ts index 2b329c0cbc0e7..594e16f619556 100644 --- a/x-pack/plugins/fleet/server/types/extensions.ts +++ b/x-pack/plugins/fleet/server/types/extensions.ts @@ -81,16 +81,6 @@ export type ExternalCallbackPostDelete = [ ]; export type ExternalCallbackUpdate = ['packagePolicyUpdate', PutPackagePolicyUpdateCallback]; -/** - * Callbacks supported by the Fleet plugin - */ -export type ExternalCallback = - | ExternalCallbackCreate - | ExternalCallbackPostCreate - | ExternalCallbackDelete - | ExternalCallbackPostDelete - | ExternalCallbackUpdate; - export type ExternalCallbackAgentPolicyCreate = [ 'agentPolicyCreate', PostAgentPolicyCreateCallback @@ -100,10 +90,16 @@ export type ExternalCallbackAgentPolicyUpdate = [ PostAgentPolicyUpdateCallback ]; -export type ExternalCallbackAgentPolicy = +/** + * Callbacks supported by the Fleet plugin + */ +export type ExternalCallback = + | ExternalCallbackCreate + | ExternalCallbackPostCreate + | ExternalCallbackDelete + | ExternalCallbackPostDelete + | ExternalCallbackUpdate | ExternalCallbackAgentPolicyCreate | ExternalCallbackAgentPolicyUpdate; -export type ExternalCallbackType = ExternalCallback | ExternalCallbackAgentPolicy; - -export type ExternalCallbacksStorage = Map>; +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/migrations/turn_off_agent_policy_features.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.ts index 0ab8915a7d385..0d1b01d68b765 100644 --- 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 @@ -30,7 +30,7 @@ export const turnOffAgentPolicyFeatures = async ( } log.info( - `App feature [${AppFeatureSecurityKey.endpointAgentTamperProtection}] is disabled. Checking endpoint integration policies for compliance` + `App feature [${AppFeatureSecurityKey.endpointAgentTamperProtection}] is disabled. Checking fleet agent policies for compliance` ); const { agentPolicy: agentPolicyService, internalSoClient } = fleetServices; @@ -76,9 +76,12 @@ export const turnOffAgentPolicyFeatures = async ( }); if (policyUpdateErrors.length > 0) { - logger.error(`Done. ${policyUpdateErrors.length} out of ${updates.length}`); - policyUpdateErrors.forEach((e) => - logger.error(`Policy [${e.id}] failed to update due to error: ${e.error}`) + 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`); 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 3840f950f9dfa..bf8f754b5ed92 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 @@ -31,7 +31,7 @@ import { getPackagePolicyPostCreateCallback, getPackagePolicyUpdateCallback, } from './fleet_integration'; -import type { KibanaRequest } from '@kbn/core/server'; +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'; @@ -52,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'; @@ -60,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', @@ -399,7 +403,7 @@ describe('ingest_integration tests ', () => { policyConfig.is_protected = true; await expect(() => callback(policyConfig)).rejects.toThrow( - 'Agent Tamper Protection requires Complete Endpoint Security tier' + '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 () => { @@ -443,58 +447,50 @@ describe('ingest_integration tests ', () => { }); describe('agent policy create 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'); + 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 ) ); - const callback = getAgentPolicyCreateCallback(logger, appFeaturesService); - - const policyConfig = generator.generateAgentPolicy(); + callback = getAgentPolicyCreateCallback(logger, appFeaturesService); policyConfig.is_protected = true; await expect(() => callback(policyConfig)).rejects.toThrow( - 'Agent Tamper Protection requires Complete Endpoint Security tier' + '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 = getAgentPolicyCreateCallback(logger, appFeaturesService); - - const policyConfig = generator.generateAgentPolicy(); - + 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 () => { - const logger = loggingSystemMock.create().get('ingest_integration.test'); - const callback = getAgentPolicyCreateCallback(logger, appFeaturesService); - - const policyConfig = generator.generateAgentPolicy(); + 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 logger = loggingSystemMock.create().get('ingest_integration.test'); - - const callback = getAgentPolicyCreateCallback(logger, appFeaturesService); - const policyConfig = generator.generateAgentPolicy(); + 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); 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 8ea68de2b571b..9ff06aef34c2d 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 @@ -292,6 +292,18 @@ export const getPackagePolicyPostCreateCallback = ( }; }; +const throwAgentTamperProtectionUnavailableError = (logger: Logger): 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('Agent Tamper Protection requires Complete Endpoint Security tier'); + throw agentTamperProtectionUnavailableError; +}; + export const getAgentPolicyCreateCallback = ( logger: Logger, appFeatures: AppFeaturesService @@ -301,9 +313,7 @@ export const getAgentPolicyCreateCallback = ( agentPolicy.is_protected && !appFeatures.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection) ) { - const error = new Error('Agent Tamper Protection requires Complete Endpoint Security tier'); - logger.error(error); - throw error; + throwAgentTamperProtectionUnavailableError(logger); } return agentPolicy; }; @@ -318,9 +328,7 @@ export const getAgentPolicyUpdateCallback = ( agentPolicy.is_protected && !appFeatures.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection) ) { - const error = new Error('Agent Tamper Protection requires Complete Endpoint Security tier'); - logger.error(error); - throw error; + throwAgentTamperProtectionUnavailableError(logger); } return agentPolicy; }; From b3c6937da4c1e2f3d3dd44bcb6685e4b7e873219 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Wed, 31 Jan 2024 15:42:12 +0100 Subject: [PATCH 09/11] cr changes --- .../migrations/turn_off_agent_policy_features.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 index 1d1ab48792ea8..b54221f300e27 100644 --- 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 @@ -135,7 +135,7 @@ describe('Turn Off Agent Policy Features Migration', () => { }); expect(logger.info).toHaveBeenCalledWith( - 'App feature [endpoint_agent_tamper_protection] is disabled. Checking endpoint integration policies for compliance' + 'App feature [endpoint_agent_tamper_protection] is disabled. Checking fleet agent policies for compliance' ); expect(logger.info).toHaveBeenCalledWith( @@ -155,9 +155,10 @@ describe('Turn Off Agent Policy Features Migration', () => { }); await callTurnOffAgentPolicyFeatures(); - expect(logger.error).toHaveBeenCalledWith(`Done. 1 out of 5`); expect(logger.error).toHaveBeenCalledWith( - `Policy [${bulkUpdateResponse![0].id}] failed to update due to error: Error: oh noo` + `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); From 06c9bc944683b9fecb9b702d89dec1044881e287 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Thu, 1 Feb 2024 18:25:22 +0100 Subject: [PATCH 10/11] cr changes --- .../server/fleet_integration/fleet_integration.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 9ff06aef34c2d..b9f2b4d540e8c 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 @@ -292,7 +292,11 @@ export const getPackagePolicyPostCreateCallback = ( }; }; -const throwAgentTamperProtectionUnavailableError = (logger: Logger): void => { +const throwAgentTamperProtectionUnavailableError = ( + logger: Logger, + policyName: string, + policyId: string +): void => { const agentTamperProtectionUnavailableError: Error & { statusCode?: number; apiPassThrough?: boolean; @@ -300,7 +304,9 @@ const throwAgentTamperProtectionUnavailableError = (logger: Logger): void => { // 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('Agent Tamper Protection requires Complete Endpoint Security tier'); + logger.error( + `Policy [${policyName}:${policyId}] error: Agent Tamper Protection requires Complete Endpoint Security tier` + ); throw agentTamperProtectionUnavailableError; }; @@ -313,7 +319,7 @@ export const getAgentPolicyCreateCallback = ( agentPolicy.is_protected && !appFeatures.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection) ) { - throwAgentTamperProtectionUnavailableError(logger); + throwAgentTamperProtectionUnavailableError(logger, agentPolicy.name, agentPolicy.id); } return agentPolicy; }; @@ -328,7 +334,7 @@ export const getAgentPolicyUpdateCallback = ( agentPolicy.is_protected && !appFeatures.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection) ) { - throwAgentTamperProtectionUnavailableError(logger); + throwAgentTamperProtectionUnavailableError(logger, agentPolicy.name, agentPolicy.id); } return agentPolicy; }; From b63e6867413e9d7f00c1fd0ffce6110f12cee937 Mon Sep 17 00:00:00 2001 From: "konrad.szwarc" Date: Thu, 1 Feb 2024 20:11:04 +0100 Subject: [PATCH 11/11] cr changes --- .../server/fleet_integration/fleet_integration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b9f2b4d540e8c..3ba3234381b82 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 @@ -294,8 +294,8 @@ export const getPackagePolicyPostCreateCallback = ( const throwAgentTamperProtectionUnavailableError = ( logger: Logger, - policyName: string, - policyId: string + policyName?: string, + policyId?: string ): void => { const agentTamperProtectionUnavailableError: Error & { statusCode?: number;