diff --git a/x-pack/plugins/fleet/server/services/backfill_agentless.test.ts b/x-pack/plugins/fleet/server/services/backfill_agentless.test.ts new file mode 100644 index 0000000000000..b0fd41fe83295 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/backfill_agentless.test.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 { backfillPackagePolicySupportsAgentless } from './backfill_agentless'; +import { packagePolicyService } from './package_policy'; + +jest.mock('.', () => ({ + appContextService: { + getLogger: () => ({ + debug: jest.fn(), + }), + getInternalUserSOClientForSpaceId: jest.fn(), + getInternalUserSOClientWithoutSpaceExtension: () => ({ + find: jest.fn().mockImplementation((options) => { + if (options.type === 'ingest-agent-policies') { + return { + saved_objects: [{ id: 'agent_policy_1' }, { id: 'agent_policy_2' }], + }; + } else { + return { + saved_objects: [ + { + id: 'package_policy_1', + attributes: { + inputs: [], + policy_ids: ['agent_policy_1'], + supports_agentless: false, + }, + }, + ], + }; + } + }), + }), + }, +})); + +jest.mock('./package_policy', () => ({ + packagePolicyService: { + update: jest.fn(), + }, + getPackagePolicySavedObjectType: jest.fn().mockResolvedValue('ingest-package-policies'), +})); + +describe('backfill agentless package policies', () => { + it('should backfill package policies missing supports_agentless', async () => { + await backfillPackagePolicySupportsAgentless(undefined as any); + + expect(packagePolicyService.update).toHaveBeenCalledWith( + undefined, + undefined, + 'package_policy_1', + { + enabled: undefined, + inputs: [], + name: undefined, + policy_ids: ['agent_policy_1'], + supports_agentless: true, + } + ); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/backfill_agentless.ts b/x-pack/plugins/fleet/server/services/backfill_agentless.ts new file mode 100644 index 0000000000000..b61265bd53c30 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/backfill_agentless.ts @@ -0,0 +1,99 @@ +/* + * 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/server'; + +import pMap from 'p-map'; + +import { MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, SO_SEARCH_LIMIT } from '../constants'; + +import type { AgentPolicySOAttributes, PackagePolicy, PackagePolicySOAttributes } from '../types'; + +import { getAgentPolicySavedObjectType } from './agent_policy'; + +import { appContextService } from '.'; +import { getPackagePolicySavedObjectType, packagePolicyService } from './package_policy'; +import { mapPackagePolicySavedObjectToPackagePolicy } from './package_policies'; + +export async function backfillPackagePolicySupportsAgentless(esClient: ElasticsearchClient) { + const apSavedObjectType = await getAgentPolicySavedObjectType(); + const internalSoClientWithoutSpaceExtension = + appContextService.getInternalUserSOClientWithoutSpaceExtension(); + const findRes = await internalSoClientWithoutSpaceExtension.find({ + type: apSavedObjectType, + page: 1, + perPage: SO_SEARCH_LIMIT, + filter: `${apSavedObjectType}.attributes.supports_agentless:true`, + fields: [`id`], + namespaces: ['*'], + }); + + const agentPolicyIds = findRes.saved_objects.map((so) => so.id); + + if (agentPolicyIds.length === 0) { + return; + } + + const savedObjectType = await getPackagePolicySavedObjectType(); + const packagePoliciesToUpdate = ( + await appContextService + .getInternalUserSOClientWithoutSpaceExtension() + .find({ + type: savedObjectType, + fields: [ + 'name', + 'policy_ids', + 'supports_agentless', + 'enabled', + 'policy_ids', + 'inputs', + 'package', + ], + filter: `${savedObjectType}.attributes.package.name:cloud_security_posture AND (NOT ${savedObjectType}.attributes.supports_agentless:true) AND ${savedObjectType}.attributes.policy_ids:(${agentPolicyIds.join( + ' OR ' + )})`, + perPage: SO_SEARCH_LIMIT, + namespaces: ['*'], + }) + ).saved_objects.map((so) => mapPackagePolicySavedObjectToPackagePolicy(so, so.namespaces)); + + appContextService + .getLogger() + .debug( + `Backfilling supports_agentless on package policies: ${packagePoliciesToUpdate.map( + (policy) => policy.id + )}` + ); + + if (packagePoliciesToUpdate.length > 0) { + const getPackagePolicyUpdate = (packagePolicy: PackagePolicy) => ({ + name: packagePolicy.name, + enabled: packagePolicy.enabled, + policy_ids: packagePolicy.policy_ids, + inputs: packagePolicy.inputs, + supports_agentless: true, + }); + + await pMap( + packagePoliciesToUpdate, + (packagePolicy) => { + const soClient = appContextService.getInternalUserSOClientForSpaceId( + packagePolicy.spaceIds?.[0] + ); + return packagePolicyService.update( + soClient, + esClient, + packagePolicy.id, + getPackagePolicyUpdate(packagePolicy) + ); + }, + { + concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, + } + ); + } +} diff --git a/x-pack/plugins/fleet/server/services/setup.test.ts b/x-pack/plugins/fleet/server/services/setup.test.ts index 8add6942e9da7..d145f264ec3e8 100644 --- a/x-pack/plugins/fleet/server/services/setup.test.ts +++ b/x-pack/plugins/fleet/server/services/setup.test.ts @@ -33,6 +33,7 @@ jest.mock('./epm/elasticsearch/template/install', () => { ...jest.requireActual('./epm/elasticsearch/template/install'), }; }); +jest.mock('./backfill_agentless'); const mockedMethodThrowsError = (mockFn: jest.Mock) => mockFn.mockImplementation(() => { diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 0d6ec183531a4..fdfc00131184f 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -57,6 +57,7 @@ import { ensureDeleteUnenrolledAgentsSetting, getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig, } from './preconfiguration/delete_unenrolled_agent_setting'; +import { backfillPackagePolicySupportsAgentless } from './backfill_agentless'; export interface SetupStatus { isInitialized: boolean; @@ -300,6 +301,9 @@ async function createSetupSideEffects( await ensureAgentPoliciesFleetServerKeysAndPolicies({ soClient, esClient, logger }); stepSpan?.end(); + logger.debug('Backfilling package policy supports_agentless field'); + await backfillPackagePolicySupportsAgentless(esClient); + const nonFatalErrors = [ ...preconfiguredPackagesNonFatalErrors, ...(messageSigningServiceNonFatalError ? [messageSigningServiceNonFatalError] : []),