From 34b0772076c688b90b74c95e0f893a62d0bba2e3 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:37:17 +1100 Subject: [PATCH] [8.x] [UII] Restrict agentless integrations to deployments with agentless enabled (#194885) (#196459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[UII] Restrict agentless integrations to deployments with agentless enabled (#194885)](https://github.com/elastic/kibana/pull/194885) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Jen Huang --- .../services/agentless_policy_helper.test.ts | 287 ++++++++++++++++++ .../services/agentless_policy_helper.ts | 41 +++ .../hooks/setup_technology.ts | 14 +- .../hooks/use_package_policy_steps.tsx | 1 - .../home/hooks/use_available_packages.tsx | 38 ++- .../plugins/fleet/public/hooks/use_config.ts | 23 +- x-pack/plugins/fleet/public/hooks/use_core.ts | 7 +- .../fleet/public/mock/plugin_interfaces.ts | 2 + x-pack/plugins/fleet/public/plugin.ts | 3 +- .../plugins/fleet/server/errors/handlers.ts | 4 + x-pack/plugins/fleet/server/errors/index.ts | 1 + .../services/epm/packages/install.test.ts | 69 ++++- .../server/services/epm/packages/install.ts | 18 ++ .../fleet/server/services/utils/agentless.ts | 7 +- 14 files changed, 488 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/fleet/common/services/agentless_policy_helper.test.ts diff --git a/x-pack/plugins/fleet/common/services/agentless_policy_helper.test.ts b/x-pack/plugins/fleet/common/services/agentless_policy_helper.test.ts new file mode 100644 index 0000000000000..aed3020c9dcf1 --- /dev/null +++ b/x-pack/plugins/fleet/common/services/agentless_policy_helper.test.ts @@ -0,0 +1,287 @@ +/* + * 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 { RegistryPolicyTemplate } from '../types'; + +import { + isAgentlessIntegration, + getAgentlessAgentPolicyNameFromPackagePolicyName, + isOnlyAgentlessIntegration, + isOnlyAgentlessPolicyTemplate, +} from './agentless_policy_helper'; + +describe('agentless_policy_helper', () => { + describe('isAgentlessIntegration', () => { + it('should return true if packageInfo is defined and has at least one agentless integration', () => { + const packageInfo = { + policy_templates: [ + { + name: 'template1', + title: 'Template 1', + description: '', + deployment_modes: { + default: { + enabled: true, + }, + agentless: { + enabled: true, + }, + }, + }, + { + name: 'template2', + title: 'Template 2', + description: '', + deployment_modes: { + default: { + enabled: true, + }, + }, + }, + ] as RegistryPolicyTemplate[], + }; + + const result = isAgentlessIntegration(packageInfo); + + expect(result).toBe(true); + }); + + it('should return false if packageInfo is defined but does not have agentless integrations', () => { + const packageInfo = { + policy_templates: [ + { + name: 'template1', + title: 'Template 1', + description: '', + deployment_modes: { + default: { + enabled: true, + }, + agentless: { + enabled: false, + }, + }, + }, + { + name: 'template2', + title: 'Template 2', + description: '', + deployment_modes: { + default: { + enabled: false, + }, + agentless: { + enabled: false, + }, + }, + }, + ] as RegistryPolicyTemplate[], + }; + + const result = isAgentlessIntegration(packageInfo); + + expect(result).toBe(false); + }); + + it('should return false if packageInfo has no policy templates', () => { + const packageInfo = { + policy_templates: [], + }; + + const result = isAgentlessIntegration(packageInfo); + + expect(result).toBe(false); + }); + + it('should return false if packageInfo is undefined', () => { + const packageInfo = undefined; + + const result = isAgentlessIntegration(packageInfo); + + expect(result).toBe(false); + }); + }); + + describe('getAgentlessAgentPolicyNameFromPackagePolicyName', () => { + it('should return the agentless agent policy name based on the package policy name', () => { + const packagePolicyName = 'example-package-policy'; + + const result = getAgentlessAgentPolicyNameFromPackagePolicyName(packagePolicyName); + + expect(result).toBe('Agentless policy for example-package-policy'); + }); + }); + + describe('isOnlyAgentlessIntegration', () => { + it('should return true if packageInfo is defined and has only agentless integration', () => { + const packageInfo = { + policy_templates: [ + { + name: 'template1', + title: 'Template 1', + description: '', + deployment_modes: { + default: { + enabled: false, + }, + agentless: { + enabled: true, + }, + }, + }, + { + name: 'template2', + title: 'Template 2', + description: '', + deployment_modes: { + agentless: { + enabled: true, + }, + }, + }, + ] as RegistryPolicyTemplate[], + }; + + const result = isOnlyAgentlessIntegration(packageInfo); + + expect(result).toBe(true); + }); + + it('should return false if packageInfo is defined but has other deployment types', () => { + const packageInfo = { + policy_templates: [ + { + name: 'template1', + title: 'Template 1', + description: '', + deployment_modes: { + default: { + enabled: true, + }, + agentless: { + enabled: true, + }, + }, + }, + { + name: 'template2', + title: 'Template 2', + description: '', + deployment_modes: { + default: { + enabled: true, + }, + }, + }, + ] as RegistryPolicyTemplate[], + }; + + const result = isOnlyAgentlessIntegration(packageInfo); + + expect(result).toBe(false); + }); + + it('should return false if packageInfo has no policy templates', () => { + const packageInfo = { + policy_templates: [], + }; + + const result = isOnlyAgentlessIntegration(packageInfo); + + expect(result).toBe(false); + }); + + it('should return false if packageInfo is undefined', () => { + const packageInfo = undefined; + + const result = isOnlyAgentlessIntegration(packageInfo); + + expect(result).toBe(false); + }); + }); + + describe('isOnlyAgentlessPolicyTemplate', () => { + it('should return true if the policy template is only agentless', () => { + const policyTemplate = { + name: 'template1', + title: 'Template 1', + description: '', + deployment_modes: { + default: { + enabled: false, + }, + agentless: { + enabled: true, + }, + }, + }; + const policyTemplate2 = { + name: 'template2', + title: 'Template 2', + description: '', + deployment_modes: { + agentless: { + enabled: true, + }, + }, + }; + + const result = isOnlyAgentlessPolicyTemplate(policyTemplate); + const result2 = isOnlyAgentlessPolicyTemplate(policyTemplate2); + + expect(result).toBe(true); + expect(result2).toBe(true); + }); + + it('should return false if the policy template has other deployment types', () => { + const policyTemplate = { + name: 'template1', + title: 'Template 1', + description: '', + deployment_modes: { + default: { + enabled: true, + }, + agentless: { + enabled: true, + }, + }, + }; + const policyTemplate2 = { + name: 'template2', + title: 'Template 2', + description: '', + deployment_modes: { + default: { + enabled: true, + }, + agentless: { + enabled: false, + }, + }, + }; + + const result = isOnlyAgentlessPolicyTemplate(policyTemplate); + const result2 = isOnlyAgentlessPolicyTemplate(policyTemplate2); + + expect(result).toBe(false); + expect(result2).toBe(false); + }); + + it('should return false if the policy template has no deployment modes', () => { + const policyTemplate = { + name: 'template1', + title: 'Template 1', + description: '', + }; + + const result = isOnlyAgentlessPolicyTemplate(policyTemplate); + + expect(result).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/fleet/common/services/agentless_policy_helper.ts b/x-pack/plugins/fleet/common/services/agentless_policy_helper.ts index ede0dfa497187..7093875ae84f5 100644 --- a/x-pack/plugins/fleet/common/services/agentless_policy_helper.ts +++ b/x-pack/plugins/fleet/common/services/agentless_policy_helper.ts @@ -5,6 +5,47 @@ * 2.0. */ +import type { PackageInfo, RegistryPolicyTemplate } from '../types'; + +export const isAgentlessIntegration = ( + packageInfo: Pick | undefined +) => { + if ( + packageInfo?.policy_templates && + packageInfo?.policy_templates.length > 0 && + !!packageInfo?.policy_templates.find( + (policyTemplate) => policyTemplate?.deployment_modes?.agentless.enabled === true + ) + ) { + return true; + } + return false; +}; + export const getAgentlessAgentPolicyNameFromPackagePolicyName = (packagePolicyName: string) => { return `Agentless policy for ${packagePolicyName}`; }; + +export const isOnlyAgentlessIntegration = ( + packageInfo: Pick | undefined +) => { + if ( + packageInfo?.policy_templates && + packageInfo?.policy_templates.length > 0 && + packageInfo?.policy_templates.every((policyTemplate) => + isOnlyAgentlessPolicyTemplate(policyTemplate) + ) + ) { + return true; + } + return false; +}; + +export const isOnlyAgentlessPolicyTemplate = (policyTemplate: RegistryPolicyTemplate) => { + return Boolean( + policyTemplate.deployment_modes && + policyTemplate.deployment_modes.agentless.enabled === true && + (!policyTemplate.deployment_modes.default || + policyTemplate.deployment_modes.default.enabled === false) + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index 95b4aa80a02bb..241dcfbb93f4e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -20,7 +20,10 @@ import { SetupTechnology } from '../../../../../types'; import { sendGetOneAgentPolicy, useStartServices } from '../../../../../hooks'; import { SelectedPolicyTab } from '../../components'; import { AGENTLESS_POLICY_ID } from '../../../../../../../../common/constants'; -import { getAgentlessAgentPolicyNameFromPackagePolicyName } from '../../../../../../../../common/services/agentless_policy_helper'; +import { + isAgentlessIntegration as isAgentlessIntegrationFn, + getAgentlessAgentPolicyNameFromPackagePolicyName, +} from '../../../../../../../../common/services/agentless_policy_helper'; export const useAgentless = () => { const config = useConfig(); @@ -45,14 +48,7 @@ export const useAgentless = () => { // When an integration has at least a policy template enabled for agentless const isAgentlessIntegration = (packageInfo: PackageInfo | undefined) => { - if ( - isAgentlessEnabled && - packageInfo?.policy_templates && - packageInfo?.policy_templates.length > 0 && - !!packageInfo?.policy_templates.find( - (policyTemplate) => policyTemplate?.deployment_modes?.agentless.enabled === true - ) - ) { + if (isAgentlessEnabled && isAgentlessIntegrationFn(packageInfo)) { return true; } return false; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx index dc055cec7fceb..1f2bdecf9e5ad 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx @@ -135,7 +135,6 @@ export function usePackagePolicySteps({ setNewAgentPolicy, updateAgentPolicies, setSelectedPolicyTab, - packageInfo, packagePolicy, isEditPage: true, agentPolicies, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx index c7b1f936e2424..2f506b30b2626 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx @@ -11,8 +11,10 @@ import { uniq } from 'lodash'; import type { CustomIntegration } from '@kbn/custom-integrations-plugin/common'; import type { IntegrationPreferenceType } from '../../../components/integration_preference'; -import { useGetPackagesQuery, useGetCategoriesQuery } from '../../../../../hooks'; +import { useAgentless } from '../../../../../../fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology'; import { + useGetPackagesQuery, + useGetCategoriesQuery, useGetAppendCustomIntegrationsQuery, useGetReplacementCustomIntegrationsQuery, } from '../../../../../hooks'; @@ -28,6 +30,11 @@ import { isIntegrationPolicyTemplate, } from '../../../../../../../../common/services'; +import { + isOnlyAgentlessPolicyTemplate, + isOnlyAgentlessIntegration, +} from '../../../../../../../../common/services/agentless_policy_helper'; + import type { IntegrationCardItem } from '..'; import { ALL_CATEGORY } from '../category_facets'; @@ -103,6 +110,23 @@ const packageListToIntegrationsList = (packages: PackageList): PackageList => { }, []); }; +// Return filtered packages based on deployment mode, +// Currently filters out agentless only packages and policy templates if agentless is not available +const filterPackageListDeploymentModes = (packages: PackageList, isAgentlessEnabled: boolean) => { + return isAgentlessEnabled + ? packages + : packages + .filter((pkg) => { + return !isOnlyAgentlessIntegration(pkg); + }) + .map((pkg) => { + pkg.policy_templates = (pkg.policy_templates || []).filter((policyTemplate) => { + return !isOnlyAgentlessPolicyTemplate(policyTemplate); + }); + return pkg; + }); +}; + export type AvailablePackagesHookType = typeof useAvailablePackages; export const useAvailablePackages = ({ @@ -113,6 +137,7 @@ export const useAvailablePackages = ({ const [preference, setPreference] = useState('recommended'); const { showIntegrationsSubcategories } = ExperimentalFeaturesService.get(); + const { isAgentlessEnabled } = useAgentless(); const { initialSelectedCategory, @@ -146,10 +171,13 @@ export const useAvailablePackages = ({ }); } - const eprIntegrationList = useMemo( - () => packageListToIntegrationsList(eprPackages?.items || []), - [eprPackages] - ); + const eprIntegrationList = useMemo(() => { + const filteredPackageList = + filterPackageListDeploymentModes(eprPackages?.items || [], isAgentlessEnabled) || []; + const integrations = packageListToIntegrationsList(filteredPackageList); + return integrations; + }, [eprPackages?.items, isAgentlessEnabled]); + const { data: replacementCustomIntegrations, isInitialLoading: isLoadingReplacmentCustomIntegrations, diff --git a/x-pack/plugins/fleet/public/hooks/use_config.ts b/x-pack/plugins/fleet/public/hooks/use_config.ts index db86ed66bba60..2df3ed5f38a54 100644 --- a/x-pack/plugins/fleet/public/hooks/use_config.ts +++ b/x-pack/plugins/fleet/public/hooks/use_config.ts @@ -9,12 +9,27 @@ import React, { useContext } from 'react'; import type { FleetConfigType } from '../plugin'; +import { useStartServices } from '.'; + export const ConfigContext = React.createContext(null); -export function useConfig() { - const config = useContext(ConfigContext); - if (config === null) { - throw new Error('ConfigContext not initialized'); +export function useConfig(): FleetConfigType { + const { fleet } = useStartServices(); + const baseConfig = useContext(ConfigContext); + + // Downstream plugins may set `fleet` as part of the Kibana context + // which means that the Fleet config is exposed in that way + const pluginConfig = fleet?.config; + const config = baseConfig || pluginConfig || null; + + if (baseConfig === null && pluginConfig) { + // eslint-disable-next-line no-console + console.warn('Fleet ConfigContext not initialized, using from plugin context'); } + + if (!config) { + throw new Error('Fleet ConfigContext not initialized'); + } + return config; } diff --git a/x-pack/plugins/fleet/public/hooks/use_core.ts b/x-pack/plugins/fleet/public/hooks/use_core.ts index 0e65686ac38a7..314e7931eb363 100644 --- a/x-pack/plugins/fleet/public/hooks/use_core.ts +++ b/x-pack/plugins/fleet/public/hooks/use_core.ts @@ -7,10 +7,11 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { FleetStartServices } from '../plugin'; +import type { FleetStart, FleetStartServices } from '../plugin'; -export function useStartServices(): FleetStartServices { - const { services } = useKibana(); +// Downstream plugins may set `fleet` as part of the Kibana context +export function useStartServices(): FleetStartServices & { fleet?: FleetStart } { + const { services } = useKibana(); if (services === null) { throw new Error('KibanaContextProvider not initialized'); } diff --git a/x-pack/plugins/fleet/public/mock/plugin_interfaces.ts b/x-pack/plugins/fleet/public/mock/plugin_interfaces.ts index e2490eecfd766..5af34f2b0bc04 100644 --- a/x-pack/plugins/fleet/public/mock/plugin_interfaces.ts +++ b/x-pack/plugins/fleet/public/mock/plugin_interfaces.ts @@ -9,6 +9,7 @@ import type { UIExtensionsStorage } from '../types'; import { createExtensionRegistrationCallback } from '../services/ui_extensions'; import type { MockedFleetStart } from './types'; +import { createConfigurationMock } from './plugin_configuration'; export const createStartMock = (extensionsStorage: UIExtensionsStorage = {}): MockedFleetStart => { return { @@ -41,6 +42,7 @@ export const createStartMock = (extensionsStorage: UIExtensionsStorage = {}): Mo writeIntegrationPolicies: true, }, }, + config: createConfigurationMock(), hooks: { epm: { getBulkAssets: jest.fn() } }, }; }; diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index ce922f838ae4e..ced047f7cc0c4 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -102,6 +102,7 @@ export interface FleetSetup {} export interface FleetStart { /** Authorization for the current user */ authz: FleetAuthz; + config: FleetConfigType; registerExtension: UIExtensionRegistrationCallback; isInitialized: () => Promise; hooks: { @@ -356,7 +357,7 @@ export class FleetPlugin implements Plugin { const permissionsResponse = await getPermissions(); diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index 31e4b9d6704c7..2bdd118e7fb40 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -45,6 +45,7 @@ import { PackageSavedObjectConflictError, FleetTooManyRequestsError, AgentlessPolicyExistsRequestError, + PackageInvalidDeploymentMode, PackagePolicyContentPackageError, } from '.'; @@ -61,6 +62,9 @@ interface IngestErrorHandlerParams { // this type is based on BadRequest values observed while debugging https://github.com/elastic/kibana/issues/75862 const getHTTPResponseCode = (error: FleetError): number => { // Bad Request + if (error instanceof PackageInvalidDeploymentMode) { + return 400; + } if (error instanceof PackageFailedVerificationError) { return 400; } diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index de528f082c096..abc36f7df9692 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -29,6 +29,7 @@ export class RegistryResponseError extends RegistryError { // Package errors +export class PackageInvalidDeploymentMode extends FleetError {} export class PackageOutdatedError extends FleetError {} export class PackageFailedVerificationError extends FleetError { constructor(pkgName: string, pkgVersion: string) { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts index a0bd8c8d77fe6..709e0d84d70fc 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts @@ -17,6 +17,7 @@ import { licenseService } from '../../license'; import { auditLoggingService } from '../../audit_logging'; import { appContextService } from '../../app_context'; import { ConcurrentInstallOperationError, FleetError, PackageNotFoundError } from '../../../errors'; +import { isAgentlessEnabled, isOnlyAgentlessIntegration } from '../../utils/agentless'; import * as Registry from '../registry'; import { dataStreamService } from '../../data_streams'; @@ -102,6 +103,13 @@ jest.mock('../archive', () => { }); jest.mock('../../audit_logging'); +jest.mock('../../utils/agentless', () => { + return { + isAgentlessEnabled: jest.fn(), + isOnlyAgentlessIntegration: jest.fn(), + }; +}); + const mockGetBundledPackageByPkgKey = jest.mocked(getBundledPackageByPkgKey); const mockedAuditLoggingService = jest.mocked(auditLoggingService); @@ -357,13 +365,72 @@ describe('install', () => { expect(response.status).toEqual('already_installed'); }); - // failing + describe('agentless', () => { + beforeEach(() => { + jest.mocked(appContextService.getConfig).mockClear(); + jest.spyOn(licenseService, 'hasAtLeast').mockClear(); + jest.mocked(isAgentlessEnabled).mockClear(); + jest.mocked(isOnlyAgentlessIntegration).mockClear(); + }); + + it('should not allow to install agentless only integration if agentless is not enabled', async () => { + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + jest.mocked(isAgentlessEnabled).mockReturnValueOnce(false); + jest.mocked(isOnlyAgentlessIntegration).mockReturnValueOnce(true); + + const response = await installPackage({ + spaceId: DEFAULT_SPACE_ID, + installSource: 'registry', + pkgkey: 'test_package', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + expect(response.error).toBeDefined(); + expect(response.error!.message).toEqual( + 'test_package contains agentless policy templates, agentless is not available on this deployment' + ); + }); + + it('should allow to install agentless only integration if agentless is not enabled but using force flag', async () => { + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + jest.mocked(isAgentlessEnabled).mockReturnValueOnce(false); + jest.mocked(isOnlyAgentlessIntegration).mockReturnValueOnce(true); + + const response = await installPackage({ + spaceId: DEFAULT_SPACE_ID, + installSource: 'registry', + pkgkey: 'test_package', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + force: true, + }); + expect(response.error).toBeUndefined(); + }); + + it('should allow to install agentless only integration if agentless is enabled', async () => { + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + jest.mocked(isAgentlessEnabled).mockReturnValueOnce(true); + jest.mocked(isOnlyAgentlessIntegration).mockReturnValueOnce(true); + + const response = await installPackage({ + spaceId: DEFAULT_SPACE_ID, + installSource: 'registry', + pkgkey: 'test_package', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + expect(response.error).toBeUndefined(); + }); + }); + it('should allow to install fleet_server if internal.fleetServerStandalone is configured', async () => { jest.mocked(appContextService.getConfig).mockReturnValueOnce({ internal: { fleetServerStandalone: true, }, } as any); + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValueOnce(true); + jest.mocked(isOnlyAgentlessIntegration).mockReturnValueOnce(false); const response = await installPackage({ spaceId: DEFAULT_SPACE_ID, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 65f1a75f76f84..1ea6f29cad839 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -60,6 +60,7 @@ import { FleetUnauthorizedError, PackageNotFoundError, FleetTooManyRequestsError, + PackageInvalidDeploymentMode, } from '../../../errors'; import { PACKAGES_SAVED_OBJECT_TYPE, @@ -82,6 +83,8 @@ import { sendTelemetryEvents, UpdateEventType } from '../../upgrade_sender'; import { auditLoggingService } from '../../audit_logging'; import { getFilteredInstallPackages } from '../filtered_packages'; +import { isAgentlessEnabled, isOnlyAgentlessIntegration } from '../../utils/agentless'; + import { _stateMachineInstallPackage } from './install_state_machine/_state_machine_package_install'; import { formatVerificationResultForSO } from './package_verification'; @@ -507,6 +510,21 @@ async function installPackageFromRegistry({ }` ); } + + // only allow install of agentless packages if agentless is enabled, or if using force flag + const agentlessEnabled = isAgentlessEnabled(); + const agentlessOnlyIntegration = isOnlyAgentlessIntegration(packageInfo); + if (!agentlessEnabled && agentlessOnlyIntegration) { + if (!force) { + throw new PackageInvalidDeploymentMode( + `${pkgkey} contains agentless policy templates, agentless is not available on this deployment` + ); + } + logger.debug( + `${pkgkey} contains agentless policy templates, agentless is not available on this deployment but installing anyway due to force flag` + ); + } + return await installPackageWithStateMachine({ pkgName, pkgVersion, diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.ts b/x-pack/plugins/fleet/server/services/utils/agentless.ts index 4c27d583d9a79..c43f10db16b46 100644 --- a/x-pack/plugins/fleet/server/services/utils/agentless.ts +++ b/x-pack/plugins/fleet/server/services/utils/agentless.ts @@ -7,14 +7,15 @@ import { appContextService } from '..'; import type { FleetConfigType } from '../../config'; +export { isOnlyAgentlessIntegration } from '../../../common/services/agentless_policy_helper'; export const isAgentlessApiEnabled = () => { - const cloudSetup = appContextService.getCloud(); + const cloudSetup = appContextService.getCloud && appContextService.getCloud(); const isHosted = cloudSetup?.isCloudEnabled || cloudSetup?.isServerlessEnabled; return Boolean(isHosted && appContextService.getConfig()?.agentless?.enabled); }; export const isDefaultAgentlessPolicyEnabled = () => { - const cloudSetup = appContextService.getCloud(); + const cloudSetup = appContextService.getCloud && appContextService.getCloud(); return Boolean( cloudSetup?.isServerlessEnabled && appContextService.getExperimentalFeatures().agentless ); @@ -44,7 +45,7 @@ export const prependAgentlessApiBasePathToEndpoint = ( agentlessConfig: FleetConfigType['agentless'], endpoint: AgentlessApiEndpoints ) => { - const cloudSetup = appContextService.getCloud(); + const cloudSetup = appContextService.getCloud && appContextService.getCloud(); const endpointPrefix = cloudSetup?.isServerlessEnabled ? AGENTLESS_SERVERLESS_API_BASE_PATH : AGENTLESS_ESS_API_BASE_PATH;