[8.x] [UII] Restrict agentless integrations to deployments with agentless enabled (#194885) (#196459)

# 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)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Jen
Huang","email":"its.jenetic@gmail.com"},"sourceCommit":{"committedDate":"2024-10-15T23:57:32Z","message":"[UII]
Restrict agentless integrations to deployments with agentless enabled
(#194885)\n\n## Summary\r\n\r\nResolves #192486. This PR makes it so
that on deployments without\r\nagentless enabled:\r\n1. Agentless-only
integrations are hidden from the browse integration UI\r\n2.
Agentless-only integrations cannot be installed via API (unless
force\r\nflag is used)\r\n\r\n⚠️
https://github.com/elastic/package-registry/issues/1238 needs to
be\r\ncompleted for the below testing steps to work. Currently EPR does
not\r\nreturn `deployment_modes` property which is necessary for Fleet
to know\r\nwhich packages are agentless.\r\n\r\n## How to test\r\n\r\n1.
Simulate agentless being available by adding the following
to\r\nkibana.yml:\r\n```\r\nxpack.fleet.agentless.enabled: true\r\n\r\n#
Simulate cloud\r\nxpack.cloud.id: \"foo\"\r\nxpack.cloud.base_url:
\"https://cloud.elastic.co\"\r\nxpack.cloud.organization_url:
\"/account/\"\r\nxpack.cloud.billing_url:
\"/billing/\"\r\nxpack.cloud.profile_url:
\"/user/settings/\"\r\n```\r\n2. Go to `Integrations > Browse` and
enable showing Beta integrations,\r\nsearch for `connector` and you
should see the agentless integrations:\r\nElastic Connectors, GitHub &
GitHub Enterprise Server Connector, Google\r\nDrive Connector\r\n3.
Install any one of them (they all come from the same package),
it\r\nshould be successful\r\n4. Uninstall them\r\n5. Remove config
changes to go back to a non-agentless deployment\r\n6. Refresh
Integrations list, the three integrations should no
longer\r\nappear\r\n7. Try installing via API, an error should
appear\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_connectors/0.0.2\r\n```\r\n8. Try
installing via API again with force flag, it should
be\r\nsuccessful:\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_connectors/0.0.2\r\n{\r\n \"force\":
true\r\n}\r\n```\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"8cadf88c66a257c073279fa11572b089c32eb643","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Fleet","v9.0.0","backport:prev-minor"],"title":"[UII]
Restrict agentless integrations to deployments with agentless
enabled","number":194885,"url":"https://github.com/elastic/kibana/pull/194885","mergeCommit":{"message":"[UII]
Restrict agentless integrations to deployments with agentless enabled
(#194885)\n\n## Summary\r\n\r\nResolves #192486. This PR makes it so
that on deployments without\r\nagentless enabled:\r\n1. Agentless-only
integrations are hidden from the browse integration UI\r\n2.
Agentless-only integrations cannot be installed via API (unless
force\r\nflag is used)\r\n\r\n⚠️
https://github.com/elastic/package-registry/issues/1238 needs to
be\r\ncompleted for the below testing steps to work. Currently EPR does
not\r\nreturn `deployment_modes` property which is necessary for Fleet
to know\r\nwhich packages are agentless.\r\n\r\n## How to test\r\n\r\n1.
Simulate agentless being available by adding the following
to\r\nkibana.yml:\r\n```\r\nxpack.fleet.agentless.enabled: true\r\n\r\n#
Simulate cloud\r\nxpack.cloud.id: \"foo\"\r\nxpack.cloud.base_url:
\"https://cloud.elastic.co\"\r\nxpack.cloud.organization_url:
\"/account/\"\r\nxpack.cloud.billing_url:
\"/billing/\"\r\nxpack.cloud.profile_url:
\"/user/settings/\"\r\n```\r\n2. Go to `Integrations > Browse` and
enable showing Beta integrations,\r\nsearch for `connector` and you
should see the agentless integrations:\r\nElastic Connectors, GitHub &
GitHub Enterprise Server Connector, Google\r\nDrive Connector\r\n3.
Install any one of them (they all come from the same package),
it\r\nshould be successful\r\n4. Uninstall them\r\n5. Remove config
changes to go back to a non-agentless deployment\r\n6. Refresh
Integrations list, the three integrations should no
longer\r\nappear\r\n7. Try installing via API, an error should
appear\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_connectors/0.0.2\r\n```\r\n8. Try
installing via API again with force flag, it should
be\r\nsuccessful:\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_connectors/0.0.2\r\n{\r\n \"force\":
true\r\n}\r\n```\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"8cadf88c66a257c073279fa11572b089c32eb643"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/194885","number":194885,"mergeCommit":{"message":"[UII]
Restrict agentless integrations to deployments with agentless enabled
(#194885)\n\n## Summary\r\n\r\nResolves #192486. This PR makes it so
that on deployments without\r\nagentless enabled:\r\n1. Agentless-only
integrations are hidden from the browse integration UI\r\n2.
Agentless-only integrations cannot be installed via API (unless
force\r\nflag is used)\r\n\r\n⚠️
https://github.com/elastic/package-registry/issues/1238 needs to
be\r\ncompleted for the below testing steps to work. Currently EPR does
not\r\nreturn `deployment_modes` property which is necessary for Fleet
to know\r\nwhich packages are agentless.\r\n\r\n## How to test\r\n\r\n1.
Simulate agentless being available by adding the following
to\r\nkibana.yml:\r\n```\r\nxpack.fleet.agentless.enabled: true\r\n\r\n#
Simulate cloud\r\nxpack.cloud.id: \"foo\"\r\nxpack.cloud.base_url:
\"https://cloud.elastic.co\"\r\nxpack.cloud.organization_url:
\"/account/\"\r\nxpack.cloud.billing_url:
\"/billing/\"\r\nxpack.cloud.profile_url:
\"/user/settings/\"\r\n```\r\n2. Go to `Integrations > Browse` and
enable showing Beta integrations,\r\nsearch for `connector` and you
should see the agentless integrations:\r\nElastic Connectors, GitHub &
GitHub Enterprise Server Connector, Google\r\nDrive Connector\r\n3.
Install any one of them (they all come from the same package),
it\r\nshould be successful\r\n4. Uninstall them\r\n5. Remove config
changes to go back to a non-agentless deployment\r\n6. Refresh
Integrations list, the three integrations should no
longer\r\nappear\r\n7. Try installing via API, an error should
appear\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_connectors/0.0.2\r\n```\r\n8. Try
installing via API again with force flag, it should
be\r\nsuccessful:\r\n```\r\nPOST
kbn:/api/fleet/epm/packages/elastic_connectors/0.0.2\r\n{\r\n \"force\":
true\r\n}\r\n```\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"8cadf88c66a257c073279fa11572b089c32eb643"}}]}]
BACKPORT-->

Co-authored-by: Jen Huang <its.jenetic@gmail.com>
This commit is contained in:
Kibana Machine 2024-10-16 12:37:17 +11:00 committed by GitHub
parent b9820cc067
commit 34b0772076
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 488 additions and 27 deletions

View file

@ -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);
});
});
});

View file

@ -5,6 +5,47 @@
* 2.0.
*/
import type { PackageInfo, RegistryPolicyTemplate } from '../types';
export const isAgentlessIntegration = (
packageInfo: Pick<PackageInfo, 'policy_templates'> | 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<PackageInfo, 'policy_templates'> | 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)
);
};

View file

@ -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;

View file

@ -135,7 +135,6 @@ export function usePackagePolicySteps({
setNewAgentPolicy,
updateAgentPolicies,
setSelectedPolicyTab,
packageInfo,
packagePolicy,
isEditPage: true,
agentPolicies,

View file

@ -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<IntegrationPreferenceType>('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,

View file

@ -9,12 +9,27 @@ import React, { useContext } from 'react';
import type { FleetConfigType } from '../plugin';
import { useStartServices } from '.';
export const ConfigContext = React.createContext<FleetConfigType | null>(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;
}

View file

@ -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<FleetStartServices>();
// Downstream plugins may set `fleet` as part of the Kibana context
export function useStartServices(): FleetStartServices & { fleet?: FleetStart } {
const { services } = useKibana<FleetStartServices & { fleet?: FleetStart }>();
if (services === null) {
throw new Error('KibanaContextProvider not initialized');
}

View file

@ -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() } },
};
};

View file

@ -102,6 +102,7 @@ export interface FleetSetup {}
export interface FleetStart {
/** Authorization for the current user */
authz: FleetAuthz;
config: FleetConfigType;
registerExtension: UIExtensionRegistrationCallback;
isInitialized: () => Promise<true>;
hooks: {
@ -356,7 +357,7 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep
// capabilities.fleetv2 returns fleet privileges and capabilities.fleet returns integrations privileges
return {
authz,
config: this.config,
isInitialized: once(async () => {
const permissionsResponse = await getPermissions();

View file

@ -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;
}

View file

@ -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) {

View file

@ -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,

View file

@ -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,

View file

@ -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;