x-pack/platform/plugins/shared/fleet/server/services/agent_policy.ts (1,805 lines of code) (raw):

/* * 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 { groupBy, isEqual, keyBy, omit, pick, uniq } from 'lodash'; import { v5 as uuidv5 } from 'uuid'; import { dump } from 'js-yaml'; import pMap from 'p-map'; import { lt } from 'semver'; import type { AuthenticatedUser, ElasticsearchClient, SavedObjectsBulkUpdateObject, SavedObjectsBulkUpdateResponse, SavedObjectsClientContract, SavedObject, SavedObjectsUpdateResponse, SavedObjectsFindOptions, } from '@kbn/core/server'; import { SavedObjectsUtils } from '@kbn/core/server'; import type { BulkResponseItem } from '@elastic/elasticsearch/lib/api/types'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; import type { SavedObjectError } from '@kbn/core-saved-objects-common'; import { withSpan } from '@kbn/apm-utils'; import { getAllowedOutputTypesForAgentPolicy, packageToPackagePolicy, policyHasAPMIntegration, policyHasEndpointSecurity, policyHasFleetServer, policyHasSyntheticsIntegration, } from '../../common/services'; import type { HTTPAuthorizationHeader } from '../../common/http_authorization_header'; import { LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, AGENTS_PREFIX, FLEET_AGENT_POLICIES_SCHEMA_VERSION, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT, } from '../constants'; import type { AgentPolicy, AgentPolicySOAttributes, ExternalCallback, FullAgentPolicy, ListWithKuery, NewAgentPolicy, NewPackagePolicy, PackagePolicy, PackagePolicySOAttributes, PostAgentPolicyCreateCallback, PostAgentPolicyUpdateCallback, PreconfiguredAgentPolicy, OutputsForAgentPolicy, PostAgentPolicyPostUpdateCallback, } from '../types'; import { AGENT_POLICY_INDEX, agentPolicyStatuses, FLEET_ELASTIC_AGENT_PACKAGE, UUID_V5_NAMESPACE, AGENT_POLICY_SAVED_OBJECT_TYPE, } from '../../common/constants'; import type { DeleteAgentPolicyResponse, FetchAllAgentPoliciesOptions, FetchAllAgentPolicyIdsOptions, FleetServerPolicy, IntegrationsOutput, PackageInfo, } from '../../common/types'; import { AgentPolicyNameExistsError, AgentPolicyNotFoundError, AgentPolicyInvalidError, FleetError, FleetUnauthorizedError, HostedAgentPolicyRestrictionRelatedError, PackagePolicyRestrictionRelatedError, AgentlessPolicyExistsRequestError, OutputNotFoundError, } from '../errors'; import type { FullAgentConfigMap } from '../../common/types/models/agent_cm'; import { fullAgentConfigMapToYaml } from '../../common/services/agent_cm_to_yaml'; import { MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS_20, } from '../constants'; import { appContextService } from '.'; import { mapAgentPolicySavedObjectToAgentPolicy } from './agent_policies/utils'; import { elasticAgentManagedManifest, elasticAgentStandaloneManifest, } from './elastic_agent_manifest'; import { bulkInstallPackages } from './epm/packages'; import { getAgentsByKuery } from './agents'; import { getPackagePolicySavedObjectType, packagePolicyService } from './package_policy'; import { incrementPackagePolicyCopyName } from './package_policies'; import { outputService } from './output'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { escapeSearchQueryPhrase, normalizeKuery as _normalizeKuery } from './saved_object'; import { getFullAgentPolicy, validateOutputForPolicy, validateRequiredVersions, } from './agent_policies'; import { auditLoggingService } from './audit_logging'; import { licenseService } from './license'; import { createSoFindIterable } from './utils/create_so_find_iterable'; import { isAgentlessEnabled } from './utils/agentless'; import { validatePolicyNamespaceForSpace } from './spaces/policy_namespaces'; import { isSpaceAwarenessEnabled } from './spaces/helpers'; import { agentlessAgentService } from './agents/agentless_agent'; import { scheduleDeployAgentPoliciesTask } from './agent_policies/deploy_agent_policies_task'; const KEY_EDITABLE_FOR_MANAGED_POLICIES = ['namespace']; function normalizeKuery(savedObjectType: string, kuery: string) { if (savedObjectType === LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE) { return _normalizeKuery( savedObjectType, kuery.replace( new RegExp(`${AGENT_POLICY_SAVED_OBJECT_TYPE}\\.`, 'g'), `${savedObjectType}.attributes.` ) ); } else { return _normalizeKuery( savedObjectType, kuery.replace( new RegExp(`${LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE}\\.`, 'g'), `${savedObjectType}.attributes.` ) ); } } export async function getAgentPolicySavedObjectType() { return (await isSpaceAwarenessEnabled()) ? AGENT_POLICY_SAVED_OBJECT_TYPE : LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE; } class AgentPolicyService { private triggerAgentPolicyUpdatedEvent = async ( esClient: ElasticsearchClient, action: 'created' | 'updated' | 'deleted', agentPolicyId: string, options?: { skipDeploy?: boolean; spaceId?: string; agentPolicy?: AgentPolicy | null } ) => { return agentPolicyUpdateEventHandler(esClient, action, agentPolicyId, options); }; private async _update( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, id: string, agentPolicy: Partial<AgentPolicySOAttributes>, user?: AuthenticatedUser, options: { bumpRevision: boolean; removeProtection: boolean; skipValidation: boolean; returnUpdatedPolicy?: boolean; asyncDeploy?: boolean; } = { bumpRevision: true, removeProtection: false, skipValidation: false, returnUpdatedPolicy: true, asyncDeploy: false, } ): Promise<AgentPolicy> { const savedObjectType = await getAgentPolicySavedObjectType(); const logger = appContextService.getLogger(); logger.debug(`Starting update of agent policy ${id}`); const existingAgentPolicy = await this.get(soClient, id, true); auditLoggingService.writeCustomSoAuditLog({ action: 'update', id, name: existingAgentPolicy?.name, savedObjectType, }); if (!existingAgentPolicy) { throw new AgentPolicyNotFoundError('Agent policy not found'); } if ( existingAgentPolicy.status === agentPolicyStatuses.Inactive && agentPolicy.status !== agentPolicyStatuses.Active ) { throw new FleetError( `Agent policy ${id} cannot be updated because it is ${existingAgentPolicy.status}` ); } if (options.removeProtection) { logger.warn(`Setting tamper protection for Agent Policy ${id} to false`); } if (!options.skipValidation) { await validateOutputForPolicy( soClient, agentPolicy, existingAgentPolicy, getAllowedOutputTypesForAgentPolicy({ ...existingAgentPolicy, ...agentPolicy }) ); } await soClient.update<AgentPolicySOAttributes>(savedObjectType, id, { ...agentPolicy, ...(options.bumpRevision ? { revision: existingAgentPolicy.revision + 1 } : {}), ...(options.removeProtection ? { is_protected: false } : { is_protected: agentPolicy.is_protected }), updated_at: new Date().toISOString(), updated_by: user ? user.username : 'system', }); const newAgentPolicy = await this.get(soClient, id, false); newAgentPolicy!.package_policies = existingAgentPolicy.package_policies; if (options.bumpRevision || options.removeProtection) { if (!options.asyncDeploy) { await this.triggerAgentPolicyUpdatedEvent(esClient, 'updated', id, { spaceId: soClient.getCurrentNamespace(), agentPolicy: newAgentPolicy, }); } else { await scheduleDeployAgentPoliciesTask(appContextService.getTaskManagerStart()!, [ { id, spaceId: soClient.getCurrentNamespace(), }, ]); } } logger.debug( `Agent policy ${id} update completed, revision: ${ options.bumpRevision ? existingAgentPolicy.revision + 1 : existingAgentPolicy.revision }` ); if (options.returnUpdatedPolicy !== false) { return (await this.get(soClient, id)) as AgentPolicy; } return newAgentPolicy as AgentPolicy; } public async ensurePreconfiguredAgentPolicy( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, config: PreconfiguredAgentPolicy ): Promise<{ created: boolean; policy?: AgentPolicy; }> { const { id, space_id: kibanaSpaceId, ...preconfiguredAgentPolicy } = omit(config, 'package_policies'); const newAgentPolicyDefaults: Pick<NewAgentPolicy, 'namespace' | 'monitoring_enabled'> = { namespace: 'default', monitoring_enabled: ['logs', 'metrics'], }; const newAgentPolicy: NewAgentPolicy = { ...newAgentPolicyDefaults, ...preconfiguredAgentPolicy, is_preconfigured: true, }; if (!id) throw new AgentPolicyNotFoundError('Missing ID'); return await this.ensureAgentPolicy(soClient, esClient, newAgentPolicy, id as string); } private async ensureAgentPolicy( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, newAgentPolicy: NewAgentPolicy, id: string ): Promise<{ created: boolean; policy: AgentPolicy; }> { // For preconfigured policies with a specified ID const agentPolicy = await this.get(soClient, id, false).catch(() => null); if (!agentPolicy) { return { created: true, policy: await this.create(soClient, esClient, newAgentPolicy, { id }), }; } return { created: false, policy: agentPolicy, }; } public hasAPMIntegration(agentPolicy: AgentPolicy) { return policyHasAPMIntegration(agentPolicy); } public hasFleetServerIntegration(agentPolicy: AgentPolicy) { return policyHasFleetServer(agentPolicy); } public hasSyntheticsIntegration(agentPolicy: AgentPolicy) { return policyHasSyntheticsIntegration(agentPolicy); } public async runExternalCallbacks( externalCallbackType: ExternalCallback[0], agentPolicy: NewAgentPolicy | Partial<AgentPolicy> | AgentPolicy ): Promise<NewAgentPolicy | Partial<AgentPolicy> | AgentPolicy> { 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<AgentPolicy> ); updatedNewAgentPolicy = result; } if (externalCallbackType === 'agentPolicyPostUpdate') { result = await (callback as PostAgentPolicyPostUpdateCallback)( newAgentPolicy as AgentPolicy ); 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, agentPolicy: NewAgentPolicy, options: { id?: string; user?: AuthenticatedUser; authorizationHeader?: HTTPAuthorizationHeader | null; skipDeploy?: boolean; hasFleetServer?: boolean; } = {} ): Promise<AgentPolicy> { const savedObjectType = await getAgentPolicySavedObjectType(); // Ensure an ID is provided, so we can include it in the audit logs below if (!options.id) { options.id = SavedObjectsUtils.generateId(); } auditLoggingService.writeCustomSoAuditLog({ action: 'create', id: options.id, name: agentPolicy.name, savedObjectType, }); await this.runExternalCallbacks('agentPolicyCreate', agentPolicy); this.checkTamperProtectionLicense(agentPolicy); const logger = appContextService.getLogger(); logger.debug(`Creating new agent policy`); if (agentPolicy?.is_protected) { logger.warn( 'Agent policy requires Elastic Defend integration to set tamper protection to true' ); } this.checkAgentless(agentPolicy); await this.requireUniqueName(soClient, agentPolicy); await validatePolicyNamespaceForSpace({ spaceId: soClient.getCurrentNamespace(), namespace: agentPolicy.namespace, }); const policyForOutputValidation = { ...agentPolicy, has_fleet_server: options?.hasFleetServer, }; await validateOutputForPolicy( soClient, policyForOutputValidation, {}, getAllowedOutputTypesForAgentPolicy(policyForOutputValidation) ); validateRequiredVersions(agentPolicy.name, agentPolicy.required_versions); const newSo = await soClient.create<AgentPolicySOAttributes>( savedObjectType, { ...agentPolicy, status: 'active', is_managed: agentPolicy.is_managed ?? false, revision: 1, updated_at: new Date().toISOString(), updated_by: options?.user?.username || 'system', schema_version: FLEET_AGENT_POLICIES_SCHEMA_VERSION, is_protected: false, } as AgentPolicy, options ); await appContextService .getUninstallTokenService() ?.scoped(soClient.getCurrentNamespace()) ?.generateTokenForPolicyId(newSo.id); await this.triggerAgentPolicyUpdatedEvent(esClient, 'created', newSo.id, { skipDeploy: options.skipDeploy, spaceId: soClient.getCurrentNamespace(), }); logger.debug(`Created new agent policy with id ${newSo.id}`); return { id: newSo.id, ...newSo.attributes }; } public async requireUniqueName( soClient: SavedObjectsClientContract, givenPolicy: { id?: string; name: string; supports_agentless?: boolean | null } ) { const savedObjectType = await getAgentPolicySavedObjectType(); const results = await soClient.find<AgentPolicySOAttributes>({ type: savedObjectType, searchFields: ['name'], search: escapeSearchQueryPhrase(givenPolicy.name), }); const idsWithName = results.total && results.saved_objects.map(({ id }) => id); if (Array.isArray(idsWithName)) { const isEditingSelf = givenPolicy.id && idsWithName.includes(givenPolicy.id); if ( (!givenPolicy?.supports_agentless && !givenPolicy.id) || (!givenPolicy?.supports_agentless && !isEditingSelf) ) { const isSinglePolicy = idsWithName.length === 1; const existClause = isSinglePolicy ? `Agent Policy '${idsWithName[0]}' already exists` : `Agent Policies '${idsWithName.join(',')}' already exist`; throw new AgentPolicyNameExistsError(`${existClause} with name '${givenPolicy.name}'`); } if (givenPolicy?.supports_agentless && !givenPolicy.id) { const integrationName = givenPolicy.name.split(' ').pop(); throw new AgentlessPolicyExistsRequestError( `${givenPolicy.name} already exist. Please rename the integration name ${integrationName}.` ); } } } public async get( soClient: SavedObjectsClientContract, id: string, withPackagePolicies: boolean = true ): Promise<AgentPolicy | null> { const savedObjectType = await getAgentPolicySavedObjectType(); const agentPolicySO = await soClient.get<AgentPolicySOAttributes>(savedObjectType, id); if (!agentPolicySO) { return null; } if (agentPolicySO.error) { throw new FleetError(agentPolicySO.error.message); } const agentPolicy = mapAgentPolicySavedObjectToAgentPolicy(agentPolicySO); if (withPackagePolicies) { agentPolicy.package_policies = (await packagePolicyService.findAllForAgentPolicy(soClient, id)) || []; } auditLoggingService.writeCustomSoAuditLog({ action: 'get', id, name: agentPolicy.name, savedObjectType, }); return agentPolicy; } public async getByIds( soClient: SavedObjectsClientContract, ids: Array<string | { id: string; spaceId?: string }>, options: { fields?: string[]; withPackagePolicies?: boolean; ignoreMissing?: boolean } = {} ): Promise<AgentPolicy[]> { const savedObjectType = await getAgentPolicySavedObjectType(); const objects = ids.map((id) => { if (typeof id === 'string') { return { ...options, id, type: savedObjectType }; } return { ...options, id: id.id, namespaces: savedObjectType === AGENT_POLICY_SAVED_OBJECT_TYPE && id.spaceId ? [id.spaceId] : undefined, type: savedObjectType, }; }); const bulkGetResponse = await soClient.bulkGet<AgentPolicySOAttributes>(objects); const agentPolicies = await pMap( bulkGetResponse.saved_objects, async (agentPolicySO) => { if (agentPolicySO.error) { if (options.ignoreMissing && agentPolicySO.error.statusCode === 404) { return null; } else if (agentPolicySO.error.statusCode === 404) { throw new AgentPolicyNotFoundError(`Agent policy ${agentPolicySO.id} not found`); } else { throw new FleetError(agentPolicySO.error.message); } } const agentPolicy = mapAgentPolicySavedObjectToAgentPolicy(agentPolicySO); if (options.withPackagePolicies) { const agentPolicyWithPackagePolicies = await this.get( soClient, agentPolicySO.id, options.withPackagePolicies ); if (agentPolicyWithPackagePolicies) { agentPolicy.package_policies = agentPolicyWithPackagePolicies.package_policies; } } return agentPolicy; }, { concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS } ); const result = agentPolicies.filter( (agentPolicy): agentPolicy is AgentPolicy => agentPolicy !== null ); for (const agentPolicy of result) { auditLoggingService.writeCustomSoAuditLog({ action: 'get', id: agentPolicy.id, name: agentPolicy.name, savedObjectType, }); } return result; } public async list( soClient: SavedObjectsClientContract, options: ListWithKuery & { withPackagePolicies?: boolean; fields?: string[]; esClient?: ElasticsearchClient; withAgentCount?: boolean; spaceId?: string; } ): Promise<{ items: AgentPolicy[]; total: number; page: number; perPage: number; }> { const savedObjectType = await getAgentPolicySavedObjectType(); const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery, withPackagePolicies = false, fields, spaceId, } = options; const baseFindParams: SavedObjectsFindOptions = { type: savedObjectType, sortField, sortOrder, page, perPage, ...(fields ? { fields } : {}), }; if (spaceId) { baseFindParams.namespaces = [spaceId]; } const filter = kuery ? normalizeKuery(savedObjectType, kuery) : undefined; let agentPoliciesSO; try { agentPoliciesSO = await soClient.find<AgentPolicySOAttributes>({ ...baseFindParams, filter, }); } catch (e) { const isBadRequest = e.output?.statusCode === 400; const isKQLSyntaxError = e.message?.startsWith('KQLSyntaxError'); if (isBadRequest && !isKQLSyntaxError) { // fall back to simple search if the kuery is just a search term i.e not KQL agentPoliciesSO = await soClient.find<AgentPolicySOAttributes>({ ...baseFindParams, search: kuery, }); } else { throw e; } } const agentPolicies = agentPoliciesSO.saved_objects.map((agentPolicySO) => { const agentPolicy = mapAgentPolicySavedObjectToAgentPolicy(agentPolicySO); agentPolicy.agents = 0; return agentPolicy; }); if (options.withAgentCount || withPackagePolicies) { await pMap( agentPolicies, async (agentPolicy) => { if (withPackagePolicies) { agentPolicy.package_policies = (await packagePolicyService.findAllForAgentPolicy(soClient, agentPolicy.id)) || []; } if (options.withAgentCount) { await getAgentsByKuery(appContextService.getInternalUserESClient(), soClient, { showInactive: true, perPage: 0, page: 1, kuery: `${AGENTS_PREFIX}.policy_id:"${agentPolicy.id}"`, }).then(({ total }) => (agentPolicy.agents = total)); } else { agentPolicy.agents = 0; } return agentPolicy; }, { concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS } ); } for (const agentPolicy of agentPolicies) { auditLoggingService.writeCustomSoAuditLog({ action: 'find', id: agentPolicy.id, name: agentPolicy.name, savedObjectType, }); } return { items: agentPolicies, total: agentPoliciesSO.total, page, perPage, }; } public async update( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, id: string, agentPolicy: Partial<AgentPolicy>, options?: { user?: AuthenticatedUser; force?: boolean; spaceId?: string; authorizationHeader?: HTTPAuthorizationHeader | null; skipValidation?: boolean; bumpRevision?: boolean; } ): Promise<AgentPolicy> { const logger = appContextService.getLogger(); logger.debug(`Starting update of agent policy ${id}`); if (agentPolicy.name) { await this.requireUniqueName(soClient, { id, name: agentPolicy.name, supports_agentless: agentPolicy?.supports_agentless, }); } if (agentPolicy.namespace) { await validatePolicyNamespaceForSpace({ spaceId: soClient.getCurrentNamespace(), namespace: agentPolicy.namespace, }); } validateRequiredVersions(agentPolicy.name ?? id, agentPolicy.required_versions); const existingAgentPolicy = await this.get(soClient, id, true); if (!existingAgentPolicy) { throw new AgentPolicyNotFoundError('Agent policy not found'); } try { await this.runExternalCallbacks('agentPolicyUpdate', agentPolicy); } catch (error) { logger.error(`Error running external callbacks for agentPolicyUpdate`); if (error.apiPassThrough) { throw error; } } this.checkTamperProtectionLicense(agentPolicy); this.checkAgentless(agentPolicy); await this.checkForValidUninstallToken(agentPolicy, id); if (agentPolicy?.is_protected && !policyHasEndpointSecurity(existingAgentPolicy)) { logger.warn( 'Agent policy requires Elastic Defend integration to set tamper protection to true' ); // force agent policy to be false if elastic defend is not present agentPolicy.is_protected = false; } if (existingAgentPolicy.is_managed && !options?.force) { Object.entries(agentPolicy) .filter(([key]) => !KEY_EDITABLE_FOR_MANAGED_POLICIES.includes(key)) .forEach(([key, val]) => { if (!isEqual(existingAgentPolicy[key as keyof AgentPolicy], val)) { throw new HostedAgentPolicyRestrictionRelatedError(`Cannot update ${key}`); } }); } const { monitoring_enabled: monitoringEnabled } = agentPolicy; const packagesToInstall = []; if (!existingAgentPolicy.monitoring_enabled && monitoringEnabled?.length) { packagesToInstall.push(FLEET_ELASTIC_AGENT_PACKAGE); } if (packagesToInstall.length > 0) { await bulkInstallPackages({ savedObjectsClient: soClient, esClient, packagesToInstall, spaceId: options?.spaceId || DEFAULT_SPACE_ID, authorizationHeader: options?.authorizationHeader, force: options?.force, }); } return this._update(soClient, esClient, id, agentPolicy, options?.user, { bumpRevision: options?.bumpRevision ?? true, removeProtection: false, skipValidation: options?.skipValidation ?? false, }).then((updatedAgentPolicy) => { return this.runExternalCallbacks( 'agentPolicyPostUpdate', updatedAgentPolicy ) as unknown as AgentPolicy; }); } public async copy( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, id: string, newAgentPolicyProps: Pick<AgentPolicy, 'name' | 'description'>, options?: { user?: AuthenticatedUser } ): Promise<AgentPolicy> { const logger = appContextService.getLogger(); logger.debug(`Starting copy of agent policy ${id}`); // Copy base agent policy const baseAgentPolicy = await this.get(soClient, id, true); if (!baseAgentPolicy) { throw new AgentPolicyNotFoundError('Agent policy not found'); } if (baseAgentPolicy.package_policies?.length) { const hasManagedPackagePolicies = baseAgentPolicy.package_policies.some( (packagePolicy) => packagePolicy.is_managed ); if (hasManagedPackagePolicies) { throw new PackagePolicyRestrictionRelatedError( `Cannot copy an agent policy ${id} that contains managed package policies` ); } } const newAgentPolicy = await this.create( soClient, esClient, { ...pick(baseAgentPolicy, [ 'namespace', 'monitoring_enabled', 'inactivity_timeout', 'unenroll_timeout', 'agent_features', 'overrides', 'data_output_id', 'monitoring_output_id', 'download_source_id', 'fleet_server_host_id', 'supports_agentless', 'global_data_tags', 'agentless', 'monitoring_pprof_enabled', 'monitoring_http', 'monitoring_diagnostics', ]), ...newAgentPolicyProps, }, options ); if (baseAgentPolicy.package_policies) { // Copy non-shared package policies and append (copy n) to their names. const basePackagePolicies = baseAgentPolicy.package_policies.filter( (packagePolicy) => packagePolicy.policy_ids.length < 2 ); if (basePackagePolicies.length > 0) { const newPackagePolicies = await pMap( basePackagePolicies, async (packagePolicy: PackagePolicy) => { const { id: packagePolicyId, version, ...newPackagePolicy } = packagePolicy; const updatedPackagePolicy = { ...newPackagePolicy, name: await incrementPackagePolicyCopyName(soClient, packagePolicy.name), }; return updatedPackagePolicy; } ); await packagePolicyService.bulkCreate( soClient, esClient, newPackagePolicies.map((newPackagePolicy) => ({ ...newPackagePolicy, policy_ids: [newAgentPolicy.id], })), { ...options, bumpRevision: false, } ); } // Link shared package policies to new agent policy. const sharedBasePackagePolicies = baseAgentPolicy.package_policies.filter( (packagePolicy) => packagePolicy.policy_ids.length > 1 ); if (sharedBasePackagePolicies.length > 0) { const updatedSharedPackagePolicies = sharedBasePackagePolicies.map((packagePolicy) => ({ ...packagePolicy, policy_ids: [...packagePolicy.policy_ids, newAgentPolicy.id], })); await packagePolicyService.bulkUpdate(soClient, esClient, updatedSharedPackagePolicies); } } // Tamper protection is dependent on endpoint package policy // Match tamper protection setting to the original policy if (baseAgentPolicy.is_protected) { await this._update( soClient, esClient, newAgentPolicy.id, { is_protected: true }, options?.user, { bumpRevision: false, removeProtection: false, skipValidation: false, } ); } const policyNeedsBump = baseAgentPolicy.package_policies || baseAgentPolicy.is_protected; // bump revision if agent policy is updated after creation if (policyNeedsBump) { await this.bumpRevision(soClient, esClient, newAgentPolicy.id, { user: options?.user, }); } else { await this.deployPolicy(soClient, newAgentPolicy.id); } // Get updated agent policy with package policies and adjusted tamper protection const updatedAgentPolicy = await this.get(soClient, newAgentPolicy.id, true); if (!updatedAgentPolicy) { throw new AgentPolicyNotFoundError('Copied agent policy not found'); } logger.debug(`Completed copy of agent policy ${id}`); return updatedAgentPolicy; } public async bumpRevision( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, id: string, options?: { user?: AuthenticatedUser; removeProtection?: boolean; asyncDeploy?: boolean; skipValidation?: boolean; } ): Promise<void> { return withSpan('bump_agent_policy_revision', async () => { await this._update(soClient, esClient, id, {}, options?.user, { bumpRevision: true, removeProtection: options?.removeProtection ?? false, skipValidation: options?.skipValidation ?? true, returnUpdatedPolicy: false, asyncDeploy: options?.asyncDeploy, }); }); } /** * Remove an output from all agent policies that are using it, and replace the output by the default ones. * @param esClient * @param outputId */ public async removeOutputFromAll( esClient: ElasticsearchClient, outputId: string, options?: { force?: boolean } ) { const savedObjectType = await getAgentPolicySavedObjectType(); const agentPolicies = ( await appContextService .getInternalUserSOClientWithoutSpaceExtension() .find<AgentPolicySOAttributes>({ type: savedObjectType, fields: ['revision', 'data_output_id', 'monitoring_output_id'], searchFields: ['data_output_id', 'monitoring_output_id'], search: escapeSearchQueryPhrase(outputId), perPage: SO_SEARCH_LIMIT, namespaces: ['*'], }) ).saved_objects.map(mapAgentPolicySavedObjectToAgentPolicy); if (agentPolicies.length > 0) { const getAgentPolicy = (agentPolicy: AgentPolicy) => ({ data_output_id: agentPolicy.data_output_id === outputId ? null : agentPolicy.data_output_id, monitoring_output_id: agentPolicy.monitoring_output_id === outputId ? null : agentPolicy.monitoring_output_id, }); // Validate that the output is not used by any agent policy before updating any policy await pMap( agentPolicies, async (agentPolicy) => { const soClient = appContextService.getInternalUserSOClientForSpaceId( agentPolicy.space_ids?.[0] ); const existingAgentPolicy = await this.get(soClient, agentPolicy.id, true); if (!existingAgentPolicy) { throw new AgentPolicyNotFoundError('Agent policy not found'); } await validateOutputForPolicy( soClient, getAgentPolicy(agentPolicy), existingAgentPolicy, getAllowedOutputTypesForAgentPolicy(existingAgentPolicy) ); }, { concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, } ); await pMap( agentPolicies, (agentPolicy) => { const soClient = appContextService.getInternalUserSOClientForSpaceId( agentPolicy.space_ids?.[0] ); return this.update(soClient, esClient, agentPolicy.id, getAgentPolicy(agentPolicy), { skipValidation: true, force: options?.force, }); }, { concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, } ); } } /** * Remove a Fleet Server from all agent policies that are using it, to use the default one instead. */ public async removeFleetServerHostFromAll( esClient: ElasticsearchClient, fleetServerHostId: string, options?: { force?: boolean } ) { const savedObjectType = await getAgentPolicySavedObjectType(); const agentPolicies = ( await appContextService .getInternalUserSOClientWithoutSpaceExtension() .find<AgentPolicySOAttributes>({ type: savedObjectType, fields: ['revision', 'fleet_server_host_id'], searchFields: ['fleet_server_host_id'], search: escapeSearchQueryPhrase(fleetServerHostId), perPage: SO_SEARCH_LIMIT, namespaces: ['*'], }) ).saved_objects.map(mapAgentPolicySavedObjectToAgentPolicy); if (agentPolicies.length > 0) { await pMap( agentPolicies, (agentPolicy) => this.update( appContextService.getInternalUserSOClientForSpaceId(agentPolicy.space_ids?.[0]), esClient, agentPolicy.id, { fleet_server_host_id: null, }, { force: options?.force, } ), { concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, } ); } } private async _bumpPolicies( internalSoClientWithoutSpaceExtension: SavedObjectsClientContract, esClient: ElasticsearchClient, savedObjectsResults: Array<SavedObject<AgentPolicySOAttributes>>, options?: { user?: AuthenticatedUser } ): Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>> { const bumpedPolicies = savedObjectsResults.map( (policy): SavedObjectsBulkUpdateObject<AgentPolicySOAttributes> => { return { id: policy.id, type: policy.type, attributes: { ...policy.attributes, revision: policy.attributes.revision + 1, updated_at: new Date().toISOString(), updated_by: options?.user ? options.user.username : 'system', }, version: policy.version, namespace: policy.namespaces?.[0], }; } ); const bumpedPoliciesBySpaceId = groupBy( bumpedPolicies, (policy) => policy.namespace || DEFAULT_SPACE_ID ); const res = ( await Promise.all( Object.entries(bumpedPoliciesBySpaceId).map(([spaceId, policies]) => internalSoClientWithoutSpaceExtension.bulkUpdate<AgentPolicySOAttributes>(policies, { namespace: spaceId, }) ) ) ).reduce( (acc, r) => { if (r?.saved_objects) { acc.saved_objects.push(...r.saved_objects); } return acc; }, { saved_objects: [], } ); await scheduleDeployAgentPoliciesTask( appContextService.getTaskManagerStart()!, savedObjectsResults.map((policy) => ({ id: policy.id, spaceId: policy.namespaces?.[0], })) ); return res; } public async bumpAllAgentPoliciesForOutput( esClient: ElasticsearchClient, outputId: string, options?: { user?: AuthenticatedUser } ): Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>> { const { useSpaceAwareness } = appContextService.getExperimentalFeatures(); const internalSoClientWithoutSpaceExtension = appContextService.getInternalUserSOClientWithoutSpaceExtension(); const savedObjectType = await getAgentPolicySavedObjectType(); // All agent policies directly using output const agentPoliciesUsingOutput = await internalSoClientWithoutSpaceExtension.find<AgentPolicySOAttributes>({ type: savedObjectType, fields: ['revision', 'data_output_id', 'monitoring_output_id', 'namespaces'], searchFields: ['data_output_id', 'monitoring_output_id'], search: escapeSearchQueryPhrase(outputId), perPage: SO_SEARCH_LIMIT, namespaces: ['*'], }); // All package policies directly using output const packagePoliciesUsingOutput = await internalSoClientWithoutSpaceExtension.find<PackagePolicySOAttributes>({ type: await getPackagePolicySavedObjectType(), fields: ['output_id', 'namespaces', 'policy_ids'], searchFields: ['output_id'], search: escapeSearchQueryPhrase(outputId), perPage: SO_SEARCH_LIMIT, namespaces: ['*'], }); const agentPolicyIdsDirectlyUsingOutput = agentPoliciesUsingOutput.saved_objects.map( (agentPolicySO) => agentPolicySO.id ); const agentPolicyIdsOfPackagePoliciesUsingOutput = packagePoliciesUsingOutput.saved_objects.reduce((acc: Set<string>, packagePolicySO) => { const newIds = packagePolicySO.attributes.policy_ids.filter((policyId) => { return !agentPolicyIdsDirectlyUsingOutput.includes(policyId); }); return new Set([...acc, ...newIds]); }, new Set<string>()); // Agent policies of the identified package policies, excluding ones already retrieved directly const agentPoliciesOfPackagePoliciesUsingOutput = await internalSoClientWithoutSpaceExtension.bulkGet<AgentPolicySOAttributes>( [...agentPolicyIdsOfPackagePoliciesUsingOutput].map((id) => ({ type: savedObjectType, id, fields: ['revision', 'data_output_id', 'monitoring_output_id', 'namespaces'], ...(useSpaceAwareness ? { namespaces: ['*'] } : {}), })) ); return this._bumpPolicies( internalSoClientWithoutSpaceExtension, esClient, [ ...agentPoliciesUsingOutput.saved_objects, ...agentPoliciesOfPackagePoliciesUsingOutput.saved_objects, ], options ); } public async bumpAllAgentPolicies( esClient: ElasticsearchClient, options?: { user?: AuthenticatedUser } ): Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>> { const internalSoClientWithoutSpaceExtension = appContextService.getInternalUserSOClientWithoutSpaceExtension(); const savedObjectType = await getAgentPolicySavedObjectType(); const currentPolicies = await internalSoClientWithoutSpaceExtension.find<AgentPolicySOAttributes>({ type: savedObjectType, fields: ['name', 'revision', 'namespaces'], perPage: SO_SEARCH_LIMIT, namespaces: ['*'], }); return this._bumpPolicies( internalSoClientWithoutSpaceExtension, esClient, currentPolicies.saved_objects, options ); } public async delete( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, id: string, options?: { force?: boolean; user?: AuthenticatedUser } ): Promise<DeleteAgentPolicyResponse> { const logger = appContextService.getLogger(); logger.debug(`Deleting agent policy ${id}`); const savedObjectType = await getAgentPolicySavedObjectType(); const agentPolicy = await this.get(soClient, id, false); auditLoggingService.writeCustomSoAuditLog({ action: 'delete', id, name: agentPolicy?.name, savedObjectType, }); if (!agentPolicy) { throw new AgentPolicyNotFoundError('Agent policy not found'); } if (agentPolicy.is_managed && !options?.force) { throw new HostedAgentPolicyRestrictionRelatedError(`Cannot delete hosted agent policy ${id}`); } // Prevent deleting policy when assigned agents are inactive const { total } = await getAgentsByKuery(esClient, soClient, { showInactive: true, perPage: 0, page: 1, kuery: `${AGENTS_PREFIX}.policy_id:"${id}"`, }); if (total > 0 && !agentPolicy?.supports_agentless) { throw new FleetError( 'Cannot delete an agent policy that is assigned to any active or inactive agents' ); } if (agentPolicy?.supports_agentless) { logger.debug(`Starting unenrolling agent from agentless policy ${id}`); // unenroll offline agents for agentless policies first to avoid 404 Save Object error await this.triggerAgentPolicyUpdatedEvent(esClient, 'deleted', id, { spaceId: soClient.getCurrentNamespace(), }); try { // Deleting agentless deployment await agentlessAgentService.deleteAgentlessAgent(id); logger.debug( `[Agentless API] Successfully deleted agentless deployment for single agent policy id ${id}` ); } catch (error) { logger.error( `[Agentless API] Error deleting agentless deployment for single agent policy id ${id}` ); logger.error(error); } } const packagePolicies = await packagePolicyService.findAllForAgentPolicy(soClient, id); if (packagePolicies.length) { const hasManagedPackagePolicies = packagePolicies.some( (packagePolicy) => packagePolicy.is_managed ); if (hasManagedPackagePolicies && !options?.force) { throw new PackagePolicyRestrictionRelatedError( `Cannot delete agent policy ${id} that contains managed package policies` ); } const { policiesWithSingleAP: packagePoliciesToDelete, policiesWithMultipleAP } = this.packagePoliciesWithSingleAndMultiplePolicies(packagePolicies); if (packagePoliciesToDelete.length > 0) { await packagePolicyService.delete( soClient, esClient, packagePoliciesToDelete.map((p) => p.id), { force: options?.force, skipUnassignFromAgentPolicies: true, } ); logger.debug( `Deleted package policies with single agent policy with ids ${packagePoliciesToDelete .map((policy) => policy.id) .join(', ')}` ); } if (policiesWithMultipleAP.length > 0) { await packagePolicyService.bulkUpdate( soClient, esClient, policiesWithMultipleAP.map((policy) => { const newPolicyIds = policy.policy_ids.filter((policyId) => policyId !== id); return { ...policy, policy_id: newPolicyIds[0], policy_ids: newPolicyIds, }; }) ); logger.debug( `Updated package policies with multiple agent policies with ids ${policiesWithMultipleAP .map((policy) => policy.id) .join(', ')}` ); } } if (agentPolicy.is_preconfigured && !options?.force) { await soClient.create(PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, { id: String(id), }); } await soClient.delete(savedObjectType, id, { force: true, // need to delete through multiple space }); if (!agentPolicy?.supports_agentless) { await this.triggerAgentPolicyUpdatedEvent(esClient, 'deleted', id, { spaceId: soClient.getCurrentNamespace(), }); } // cleanup .fleet-policies docs on delete await this.deleteFleetServerPoliciesForPolicyId(esClient, id); logger.debug(`Deleted agent policy ${id}`); return { id, name: agentPolicy.name, }; } public async deployPolicy( soClient: SavedObjectsClientContract, agentPolicyId: string, agentPolicy?: AgentPolicy | null ) { await this.deployPolicies(soClient, [agentPolicyId], agentPolicy ? [agentPolicy] : undefined); } public async deployPolicies( soClient: SavedObjectsClientContract, agentPolicyIds: string[], agentPolicies?: AgentPolicy[] ) { // Use internal ES client so we have permissions to write to .fleet* indices const esClient = appContextService.getInternalUserESClient(); const defaultOutputId = await outputService.getDefaultDataOutputId(soClient); if (!defaultOutputId) { return; } for (const policyId of agentPolicyIds) { auditLoggingService.writeCustomAuditLog({ message: `User deploying policy [id=${policyId}]`, }); } const policies = await agentPolicyService.getByIds(soClient, agentPolicyIds); const policiesMap = keyBy(policies, 'id'); const fullPolicies = await pMap( agentPolicyIds, // There are some potential performance concerns around using `getFullAgentPolicy` in this context, e.g. // re-fetching outputs, settings, and upgrade download source URI data for each policy. This could potentially // be a bottleneck in environments with several thousand agent policies being deployed here. (agentPolicyId) => agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId, { agentPolicy: agentPolicies?.find((policy) => policy.id === agentPolicyId), }), { concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS_20, } ); const fleetServerPolicies = fullPolicies.reduce((acc, fullPolicy) => { if (!fullPolicy || !fullPolicy.revision) { return acc; } const policy = policiesMap[fullPolicy.id]; if (!policy) { return acc; } const fleetServerPolicy: FleetServerPolicy = { '@timestamp': new Date().toISOString(), revision_idx: fullPolicy.revision, coordinator_idx: 0, namespaces: fullPolicy.namespaces, data: fullPolicy as unknown as FleetServerPolicy['data'], policy_id: fullPolicy.id, default_fleet_server: policy.is_default_fleet_server === true, }; acc.push(fleetServerPolicy); return acc; }, [] as FleetServerPolicy[]); appContextService .getLogger() .debug( `Deploying policies: ${fleetServerPolicies .map((pol) => `${pol.policy_id}:${pol.revision_idx}`) .join(', ')}` ); const fleetServerPoliciesBulkBody = fleetServerPolicies.flatMap((fleetServerPolicy) => [ { index: { _id: uuidv5( `${fleetServerPolicy.policy_id}:${fleetServerPolicy.revision_idx}`, uuidv5.DNS ), }, }, fleetServerPolicy, ]); const bulkResponse = await esClient.bulk({ index: AGENT_POLICY_INDEX, operations: fleetServerPoliciesBulkBody, refresh: 'wait_for', }); if (bulkResponse.errors) { const logger = appContextService.getLogger(); const erroredDocuments = bulkResponse.items.reduce((acc, item) => { const value: BulkResponseItem | undefined = item.index; if (!value || !value.error) { return acc; } acc.push(value); return acc; }, [] as BulkResponseItem[]); logger.warn( `Failed to index documents during policy deployment: ${JSON.stringify(erroredDocuments)}` ); } const filteredFleetServerPolicies = fleetServerPolicies.filter((fleetServerPolicy) => { const policy = policiesMap[fleetServerPolicy.policy_id]; return ( !policy.schema_version || lt(policy.schema_version, FLEET_AGENT_POLICIES_SCHEMA_VERSION) ); }); await pMap( filteredFleetServerPolicies, (fleetServerPolicy) => // There are some potential performance concerns around using `agentPolicyService.update` in this context. // This could potentially be a bottleneck in environments with several thousand agent policies being deployed here. agentPolicyService.update( soClient, esClient, fleetServerPolicy.policy_id, { schema_version: FLEET_AGENT_POLICIES_SCHEMA_VERSION, }, { force: true } ), { concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, } ); } public async deleteFleetServerPoliciesForPolicyId( esClient: ElasticsearchClient, agentPolicyId: string ) { auditLoggingService.writeCustomAuditLog({ message: `User deleting policy [id=${agentPolicyId}]`, }); let hasMore = true; while (hasMore) { const res = await esClient.deleteByQuery({ index: AGENT_POLICY_INDEX, ignore_unavailable: true, scroll_size: SO_SEARCH_LIMIT, refresh: true, query: { term: { policy_id: agentPolicyId, }, }, }); hasMore = (res.deleted ?? 0) === SO_SEARCH_LIMIT; } } public async getLatestFleetPolicy(esClient: ElasticsearchClient, agentPolicyId: string) { const res = await esClient.search<FleetServerPolicy>({ index: AGENT_POLICY_INDEX, ignore_unavailable: true, rest_total_hits_as_int: true, query: { term: { policy_id: agentPolicyId, }, }, size: 1, sort: [{ revision_idx: { order: 'desc' } }], }); if ((res.hits.total as number) === 0) { return null; } return res.hits.hits[0]._source; } public async getFullAgentConfigMap( soClient: SavedObjectsClientContract, id: string, agentVersion: string, options?: { standalone: boolean } ): Promise<string | null> { const fullAgentPolicy = await getFullAgentPolicy(soClient, id, options); if (fullAgentPolicy) { const fullAgentConfigMap: FullAgentConfigMap = { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'agent-node-datastreams', namespace: 'kube-system', labels: { 'k8s-app': 'elastic-agent', }, }, data: { 'agent.yml': fullAgentPolicy, }, }; const configMapYaml = fullAgentConfigMapToYaml(fullAgentConfigMap, dump); const updateManifestVersion = elasticAgentStandaloneManifest.replace('VERSION', agentVersion); const fixedAgentYML = configMapYaml.replace('agent.yml:', 'agent.yml: |-'); return [fixedAgentYML, updateManifestVersion].join('\n'); } else { return ''; } } public async getFullAgentManifest( fleetServer: string, enrolToken: string, agentVersion: string ): Promise<string | null> { const updateManifestVersion = elasticAgentManagedManifest.replace('VERSION', agentVersion); let updateManifest = updateManifestVersion; if (fleetServer !== '') { updateManifest = updateManifest.replace('https://fleet-server:8220', fleetServer); } if (enrolToken !== '') { updateManifest = updateManifest.replace('token-id', enrolToken); } return updateManifest; } public async getFullAgentPolicy( soClient: SavedObjectsClientContract, id: string, options?: { standalone?: boolean; agentPolicy?: AgentPolicy } ): Promise<FullAgentPolicy | null> { return getFullAgentPolicy(soClient, id, options); } /** * Remove a download source from all agent policies that are using it, and replace the output by the default ones. * @param soClient * @param esClient * @param downloadSourceId */ public async removeDefaultSourceFromAll(esClient: ElasticsearchClient, downloadSourceId: string) { const savedObjectType = await getAgentPolicySavedObjectType(); const agentPolicies = ( await appContextService .getInternalUserSOClientWithoutSpaceExtension() .find<AgentPolicySOAttributes>({ type: savedObjectType, fields: ['revision', 'download_source_id'], searchFields: ['download_source_id'], search: escapeSearchQueryPhrase(downloadSourceId), perPage: SO_SEARCH_LIMIT, namespaces: ['*'], }) ).saved_objects.map(mapAgentPolicySavedObjectToAgentPolicy); if (agentPolicies.length > 0) { await pMap( agentPolicies, (agentPolicy) => this.update( appContextService.getInternalUserSOClientForSpaceId(agentPolicy.space_ids?.[0]), esClient, agentPolicy.id, { download_source_id: agentPolicy.download_source_id === downloadSourceId ? null : agentPolicy.download_source_id, } ), { concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, } ); } } public async bumpAllAgentPoliciesForDownloadSource( esClient: ElasticsearchClient, downloadSourceId: string, options?: { user?: AuthenticatedUser } ): Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>> { const internalSoClientWithoutSpaceExtension = appContextService.getInternalUserSOClientWithoutSpaceExtension(); const savedObjectType = await getAgentPolicySavedObjectType(); const currentPolicies = await internalSoClientWithoutSpaceExtension.find<AgentPolicySOAttributes>({ type: savedObjectType, fields: ['revision', 'download_source_id', 'namespaces'], searchFields: ['download_source_id'], search: escapeSearchQueryPhrase(downloadSourceId), perPage: SO_SEARCH_LIMIT, namespaces: ['*'], }); return this._bumpPolicies( internalSoClientWithoutSpaceExtension, esClient, currentPolicies.saved_objects, options ); } public async bumpAllAgentPoliciesForFleetServerHosts( esClient: ElasticsearchClient, fleetServerHostId: string, options?: { user?: AuthenticatedUser } ): Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>> { const internalSoClientWithoutSpaceExtension = appContextService.getInternalUserSOClientWithoutSpaceExtension(); const savedObjectType = await getAgentPolicySavedObjectType(); const currentPolicies = await internalSoClientWithoutSpaceExtension.find<AgentPolicySOAttributes>({ type: savedObjectType, fields: ['revision', 'fleet_server_host_id', 'namespaces'], searchFields: ['fleet_server_host_id'], search: escapeSearchQueryPhrase(fleetServerHostId), perPage: SO_SEARCH_LIMIT, }); return this._bumpPolicies( internalSoClientWithoutSpaceExtension, esClient, currentPolicies.saved_objects, options ); } public async bumpAgentPoliciesByIds( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, agentPolicyIds: string[], options?: { user?: AuthenticatedUser } ): Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>> { const internalSoClientWithoutSpaceExtension = appContextService.getInternalUserSOClientWithoutSpaceExtension(); const savedObjectType = await getAgentPolicySavedObjectType(); const objects = agentPolicyIds.map((id: string) => ({ id, type: savedObjectType })); const bulkGetResponse = await soClient.bulkGet<AgentPolicySOAttributes>(objects); return this._bumpPolicies( internalSoClientWithoutSpaceExtension, esClient, bulkGetResponse.saved_objects, options ); } public async getInactivityTimeouts(): Promise< Array<{ policyIds: string[]; inactivityTimeout: number }> > { const savedObjectType = await getAgentPolicySavedObjectType(); const internalSoClientWithoutSpaceExtension = appContextService.getInternalUserSOClientWithoutSpaceExtension(); const findRes = await internalSoClientWithoutSpaceExtension.find<AgentPolicySOAttributes>({ type: savedObjectType, page: 1, perPage: SO_SEARCH_LIMIT, filter: `${savedObjectType}.attributes.inactivity_timeout > 0`, fields: [`inactivity_timeout`], namespaces: ['*'], }); const groupedResults = groupBy(findRes.saved_objects, (so) => so.attributes.inactivity_timeout); return Object.entries(groupedResults).map(([inactivityTimeout, policies]) => ({ inactivityTimeout: parseInt(inactivityTimeout, 10), policyIds: policies.map((policy) => policy.id), })); } public async turnOffAgentTamperProtections(soClient: SavedObjectsClientContract): Promise<{ updatedPolicies: Array<Partial<AgentPolicy>> | null; failedPolicies: Array<{ id: string; error: Error | SavedObjectError }>; }> { const savedObjectType = await getAgentPolicySavedObjectType(); const agentPolicyFetcher = await this.fetchAllAgentPolicies(soClient, { kuery: `${savedObjectType}.is_protected: true`, }); const updatedAgentPolicies: Array<SavedObjectsUpdateResponse<AgentPolicySOAttributes>> = []; for await (const agentPolicyPageResults of agentPolicyFetcher) { const { saved_objects: bulkUpdateSavedObjects } = await soClient.bulkUpdate<AgentPolicySOAttributes>( agentPolicyPageResults.map((agentPolicy) => { const { id, revision } = agentPolicy; return { id, type: savedObjectType, attributes: { is_protected: false, revision: revision + 1, updated_at: new Date().toISOString(), updated_by: 'system', }, }; }) ); updatedAgentPolicies.push(...bulkUpdateSavedObjects); } if (!updatedAgentPolicies.length) { return { updatedPolicies: null, failedPolicies: [], }; } const failedPolicies: Array<{ id: string; error: Error | SavedObjectError; }> = []; updatedAgentPolicies.forEach((policy) => { if (policy.error) { failedPolicies.push({ id: policy.id, error: policy.error, }); } }); const updatedPoliciesSuccess = updatedAgentPolicies.filter((policy) => !policy.error); await scheduleDeployAgentPoliciesTask( appContextService.getTaskManagerStart()!, updatedPoliciesSuccess.map((policy) => ({ id: policy.id, spaceId: policy.namespaces?.[0], })) ); return { updatedPolicies: updatedPoliciesSuccess, failedPolicies }; } public async getAllManagedAgentPolicies(soClient: SavedObjectsClientContract) { const savedObjectType = await getAgentPolicySavedObjectType(); const { saved_objects: agentPolicies } = await soClient.find<AgentPolicySOAttributes>({ type: savedObjectType, page: 1, perPage: SO_SEARCH_LIMIT, filter: normalizeKuery(savedObjectType, 'ingest-agent-policies.is_managed: true'), }); return agentPolicies; } public async fetchAllAgentPolicyIds( soClient: SavedObjectsClientContract, { perPage = 1000, kuery = undefined, spaceId = undefined }: FetchAllAgentPolicyIdsOptions = {} ): Promise<AsyncIterable<string[]>> { const savedObjectType = await getAgentPolicySavedObjectType(); return createSoFindIterable<{ name: string }>({ soClient, findRequest: { type: savedObjectType, perPage, sortField: 'created_at', sortOrder: 'asc', fields: ['id', 'name'], filter: kuery ? normalizeKuery(savedObjectType, kuery) : undefined, namespaces: spaceId ? [spaceId] : undefined, }, resultsMapper: (data) => { return data.saved_objects.map((agentPolicySO) => { auditLoggingService.writeCustomSoAuditLog({ action: 'find', id: agentPolicySO.id, name: agentPolicySO.attributes.name, savedObjectType, }); return agentPolicySO.id; }); }, }); } public async fetchAllAgentPolicies( soClient: SavedObjectsClientContract, { perPage = 1000, kuery, sortOrder = 'asc', sortField = 'created_at', fields = [], spaceId = undefined, }: FetchAllAgentPoliciesOptions = {} ): Promise<AsyncIterable<AgentPolicy[]>> { const savedObjectType = await getAgentPolicySavedObjectType(); return createSoFindIterable<AgentPolicySOAttributes>({ soClient, findRequest: { type: savedObjectType, sortField, sortOrder, perPage, fields, filter: kuery ? normalizeKuery(savedObjectType, kuery) : undefined, namespaces: spaceId ? [spaceId] : undefined, }, resultsMapper(data) { return data.saved_objects.map((agentPolicySO) => { auditLoggingService.writeCustomSoAuditLog({ action: 'find', id: agentPolicySO.id, savedObjectType, }); return mapAgentPolicySavedObjectToAgentPolicy(agentPolicySO); }); }, }); } // Get all the outputs per agent policy public async getAllOutputsForPolicy( soClient: SavedObjectsClientContract, agentPolicy: AgentPolicy ) { const logger = appContextService.getLogger(); const [defaultDataOutputId, defaultMonitoringOutputId] = await Promise.all([ outputService.getDefaultDataOutputId(soClient), outputService.getDefaultMonitoringOutputId(soClient), ]); if (!defaultDataOutputId) { throw new OutputNotFoundError('Default output is not setup'); } const dataOutputId = agentPolicy.data_output_id || defaultDataOutputId; const monitoringOutputId = agentPolicy.monitoring_output_id || defaultMonitoringOutputId || dataOutputId; const outputIds = uniq([dataOutputId, monitoringOutputId]); const fetchedOutputs = await outputService.bulkGet(outputIds, { ignoreNotFound: true, }); const dataOutput = fetchedOutputs.find((output) => output.id === dataOutputId); const monitoringOutput = fetchedOutputs.find((output) => output.id === monitoringOutputId); let integrationsDataOutputs: IntegrationsOutput[] = []; if (agentPolicy?.package_policies) { const integrationsWithOutputs = agentPolicy.package_policies.filter( (pkgPolicy) => !!pkgPolicy?.output_id ); integrationsDataOutputs = await pMap( integrationsWithOutputs, async (pkgPolicy) => { if (pkgPolicy?.output_id) { try { const output = await outputService.get(soClient, pkgPolicy.output_id); return { integrationPolicyName: pkgPolicy?.name, id: output.id, name: output.name }; } catch (error) { logger.error( `error while retrieving output with id "${pkgPolicy.output_id}": ${error}` ); } } return { integrationPolicyName: pkgPolicy?.name, id: pkgPolicy?.output_id ?? '' }; }, { concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS_20, } ); } const outputs: OutputsForAgentPolicy = { monitoring: { output: { name: monitoringOutput?.name ?? '', id: monitoringOutput?.id ?? '', }, }, data: { output: { name: dataOutput?.name ?? '', id: dataOutput?.id ?? '', }, integrations: integrationsDataOutputs ?? [], }, }; return outputs; } public async listAllOutputsForPolicies( soClient: SavedObjectsClientContract, agentPolicies: AgentPolicy[] ) { const allOutputs: OutputsForAgentPolicy[] = await pMap( agentPolicies, async (agentPolicy) => { const output = await this.getAllOutputsForPolicy(soClient, agentPolicy); return { agentPolicyId: agentPolicy.id, ...output }; }, { concurrency: MAX_CONCURRENT_AGENT_POLICIES_OPERATIONS, } ); return allOutputs; } private checkTamperProtectionLicense(agentPolicy: { is_protected?: boolean }): void { if (agentPolicy?.is_protected && !licenseService.isPlatinum()) { throw new FleetUnauthorizedError('Tamper protection requires Platinum license'); } } private async checkForValidUninstallToken( agentPolicy: { is_protected?: boolean }, policyId: string ): Promise<void> { if (agentPolicy?.is_protected) { const uninstallTokenService = appContextService.getUninstallTokenService(); const uninstallTokenError = await uninstallTokenService?.checkTokenValidityForPolicy( policyId ); if (uninstallTokenError) { throw new FleetError( `Cannot enable Agent Tamper Protection: ${uninstallTokenError.error.message}` ); } } } private checkAgentless(agentPolicy: Partial<NewAgentPolicy>) { if (!isAgentlessEnabled() && agentPolicy?.supports_agentless) { throw new AgentPolicyInvalidError( 'supports_agentless is only allowed in serverless and cloud environments that support the agentless feature' ); } } private packagePoliciesWithSingleAndMultiplePolicies(packagePolicies: PackagePolicy[]): { policiesWithSingleAP: PackagePolicy[]; policiesWithMultipleAP: PackagePolicy[]; } { // Find package policies that don't have multiple agent policies and mark them for deletion const policiesWithSingleAP = packagePolicies.filter( (policy) => !policy?.policy_ids || policy?.policy_ids.length <= 1 ); const policiesWithMultipleAP = packagePolicies.filter( (policy) => policy?.policy_ids && policy?.policy_ids.length > 1 ); return { policiesWithSingleAP, policiesWithMultipleAP }; } } export const agentPolicyService = new AgentPolicyService(); export async function addPackageToAgentPolicy( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, agentPolicy: AgentPolicy, packageInfo: PackageInfo, packagePolicyName?: string, packagePolicyId?: string | number, packagePolicyDescription?: string, transformPackagePolicy?: (p: NewPackagePolicy) => NewPackagePolicy, bumpAgentPolicyRevison = false ) { const basePackagePolicy = packageToPackagePolicy( packageInfo, agentPolicy.id, agentPolicy.namespace ?? 'default', packagePolicyName, packagePolicyDescription ); const newPackagePolicy = transformPackagePolicy ? transformPackagePolicy(basePackagePolicy) : basePackagePolicy; // If an ID is provided via preconfiguration, use that value. Otherwise fall back to // a UUID v5 value seeded from the agent policy's ID and the provided package policy name. const id = packagePolicyId ? String(packagePolicyId) : uuidv5(`${agentPolicy.id}-${packagePolicyName}`, UUID_V5_NAMESPACE); await packagePolicyService.create(soClient, esClient, newPackagePolicy, { id, bumpRevision: bumpAgentPolicyRevison, skipEnsureInstalled: true, skipUniqueNameVerification: true, overwrite: true, force: true, // To add package to managed policy we need the force flag packageInfo, }); }