diff --git a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts index b57628a3f326..1843affb9bfc 100644 --- a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts +++ b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts @@ -54,7 +54,7 @@ describe('getDeprecations', () => { get: () => ({ id: 'foo', - package_policies: [''], + package_policies: [{ package: { name: 'system' } }], } as AgentPolicy), }, }), diff --git a/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts b/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts index 42e11f08d9d8..9ef90e4175a6 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts @@ -58,7 +58,10 @@ export const getCspAgentPolicies = async ( packagePolicies: PackagePolicy[], agentPolicyService: AgentPolicyServiceInterface ): Promise => - agentPolicyService.getByIds(soClient, uniq(map(packagePolicies, 'policy_id'))); + agentPolicyService.getByIds(soClient, uniq(map(packagePolicies, 'policy_id')), { + withPackagePolicies: true, + ignoreMissing: true, + }); export const getCspPackagePolicies = ( soClient: SavedObjectsClientContract, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts index 8ffeb38b6b57..4bc00f5e0209 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts @@ -111,11 +111,12 @@ const createBenchmarks = ( ); return Promise.all( agentPolicies.flatMap((agentPolicy) => { - const cspPackagesOnAgent = agentPolicy.package_policies - .map((pckPolicyId) => { - if (typeof pckPolicyId === 'string') return cspPackagePoliciesMap.get(pckPolicyId); - }) - .filter(isNonNullable); + const cspPackagesOnAgent = + agentPolicy.package_policies + ?.map(({ id: pckPolicyId }) => { + return cspPackagePoliciesMap.get(pckPolicyId); + }) + .filter(isNonNullable) ?? []; const benchmarks = cspPackagesOnAgent.map(async (cspPackage) => { const cspRulesStatus = await addPackagePolicyCspRules(soClient, cspPackage); const agentPolicyStatus = { diff --git a/x-pack/plugins/fleet/common/index.ts b/x-pack/plugins/fleet/common/index.ts index ced5c17f55f6..d514d5b56469 100644 --- a/x-pack/plugins/fleet/common/index.ts +++ b/x-pack/plugins/fleet/common/index.ts @@ -101,6 +101,8 @@ export type { UpgradePackagePolicyResponseItem, UpgradePackagePolicyBaseResponse, UpgradePackagePolicyDryRunResponseItem, + BulkGetPackagePoliciesResponse, + BulkGetAgentPoliciesResponse, // Models Agent, AgentStatus, diff --git a/x-pack/plugins/fleet/common/services/limited_package.ts b/x-pack/plugins/fleet/common/services/limited_package.ts index 601f680c8bf0..e4e9dd090941 100644 --- a/x-pack/plugins/fleet/common/services/limited_package.ts +++ b/x-pack/plugins/fleet/common/services/limited_package.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { PackageInfo, AgentPolicy, PackagePolicy } from '../types'; +import type { PackageInfo, AgentPolicy } from '../types'; export const isPackageLimited = (packageInfo: PackageInfo): boolean => { return (packageInfo.policy_templates || []).some( @@ -17,10 +17,10 @@ export const doesAgentPolicyAlreadyIncludePackage = ( agentPolicy: AgentPolicy, packageName: string ): boolean => { - if (agentPolicy.package_policies.length && typeof agentPolicy.package_policies[0] === 'string') { + if (!agentPolicy.package_policies) { throw new Error('Unable to read full package policy information'); } - return (agentPolicy.package_policies as PackagePolicy[]) + return agentPolicy.package_policies .map((packagePolicy) => packagePolicy.package?.name || '') .includes(packageName); }; diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 95551ddeea7c..323d7d1f8b37 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -118,6 +118,10 @@ export const agentPolicyRouteService = { return AGENT_POLICY_API_ROUTES.LIST_PATTERN; }, + getBulkGetPath: () => { + return AGENT_POLICY_API_ROUTES.BULK_GET_PATTERN; + }, + getInfoPath: (agentPolicyId: string) => { return AGENT_POLICY_API_ROUTES.INFO_PATTERN.replace('{agentPolicyId}', agentPolicyId); }, diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index 44e6b35c02ee..ea22f73a2e5f 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -35,7 +35,7 @@ export interface NewAgentPolicy { export interface AgentPolicy extends Omit { id: string; status: ValueOf; - package_policies: string[] | PackagePolicy[]; + package_policies?: PackagePolicy[]; is_managed: boolean; // required for created policy updated_at: string; updated_by: string; diff --git a/x-pack/plugins/fleet/cypress/downloads/downloads.html b/x-pack/plugins/fleet/cypress/downloads/downloads.html new file mode 100644 index 000000000000..772778ea352e Binary files /dev/null and b/x-pack/plugins/fleet/cypress/downloads/downloads.html differ diff --git a/x-pack/plugins/fleet/cypress/tasks/fleet.ts b/x-pack/plugins/fleet/cypress/tasks/fleet.ts index 53f2846b3a98..4a23daf7a838 100644 --- a/x-pack/plugins/fleet/cypress/tasks/fleet.ts +++ b/x-pack/plugins/fleet/cypress/tasks/fleet.ts @@ -15,9 +15,15 @@ import { } from '../screens/fleet'; export function createAgentPolicy() { + cy.intercept({ + url: '/api/fleet/agent_policies?sys_monitoring=true', + method: 'POST', + }).as('postAgentPolicy'); cy.getBySel(ADD_AGENT_BUTTON_TOP).click(); cy.getBySel(STANDALONE_TAB).click(); cy.getBySel(CREATE_POLICY_BUTTON).click(); + + cy.wait('@postAgentPolicy'); cy.getBySel(AGENT_FLYOUT_CLOSE_BUTTON).click(); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/index.tsx index c51a7415cd1a..19ff6a5d00d2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/index.tsx @@ -7,7 +7,7 @@ import React, { memo } from 'react'; -import type { AgentPolicy, PackagePolicy } from '../../../../../types'; +import type { AgentPolicy } from '../../../../../types'; import { useBreadcrumbs } from '../../../../../hooks'; import { NoPackagePolicies } from './no_package_policies'; @@ -16,14 +16,14 @@ import { PackagePoliciesTable } from './package_policies_table'; export const PackagePoliciesView = memo<{ agentPolicy: AgentPolicy }>(({ agentPolicy }) => { useBreadcrumbs('policy_details', { policyName: agentPolicy.name }); - if (agentPolicy.package_policies.length === 0) { + if (!agentPolicy.package_policies || agentPolicy.package_policies.length === 0) { return ; } return ( ); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx index ba6b7f4ad31c..9b0d9051a32e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/index.tsx @@ -89,6 +89,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { sortField: sorting?.field, sortOrder: sorting?.direction, kuery: search, + full: true, }); // Some policies retrieved, set up table props diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index a9e3429732b3..6bf9506d6ba2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -19,7 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react'; -import type { Agent, AgentPolicy, PackagePolicy, SimplifiedAgentStatus } from '../../../types'; +import type { Agent, AgentPolicy, SimplifiedAgentStatus } from '../../../types'; import { usePagination, useAuthz, @@ -35,8 +35,12 @@ import { sendGetAgentTags, } from '../../../hooks'; import { AgentEnrollmentFlyout, AgentPolicySummaryLine } from '../../../components'; -import { AgentStatusKueryHelper, isAgentUpgradeable } from '../../../services'; -import { AGENTS_PREFIX, FLEET_SERVER_PACKAGE, SO_SEARCH_LIMIT } from '../../../constants'; +import { + AgentStatusKueryHelper, + isAgentUpgradeable, + policyHasFleetServer, +} from '../../../services'; +import { AGENTS_PREFIX, SO_SEARCH_LIMIT } from '../../../constants'; import { AgentReassignAgentPolicyModal, AgentHealth, @@ -389,10 +393,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { return false; } - return agentPolicy.package_policies.some( - (ap: string | PackagePolicy) => - typeof ap !== 'string' && ap.package?.name === FLEET_SERVER_PACKAGE - ); + return policyHasFleetServer(agentPolicy); }, [agentToUnenroll, agentPoliciesIndexedById]); // Fleet server unhealthy status diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index a77c45a6b511..693dee99ebf6 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -717,11 +717,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos namespace: 'default', description: 'Default agent policy created by Kibana', status: 'active', - package_policies: [ - '4d09bd78-b0ad-4238-9fa3-d87d3c887c73', - '2babac18-eb8e-4ce4-b53b-4b7c5f507019', - 'e8a37031-2907-44f6-89d2-98bd493f60dc', - ], + package_policies: [], is_managed: false, monitoring_enabled: ['logs', 'metrics'], revision: 6, @@ -735,7 +731,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos namespace: 'default', description: 'Protect EU from COVID', status: 'active', - package_policies: ['e8a37031-2907-44f6-89d2-98bd493f60cd'], + package_policies: [], is_managed: false, monitoring_enabled: ['logs', 'metrics'], revision: 2, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts index 4f6f456bf82c..399016ebf204 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts @@ -14,7 +14,6 @@ import type { GetPackagePoliciesResponse, } from '../../../../../types'; import { agentPolicyRouteService } from '../../../../../services'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; import { useGetPackagePolicies, useConditionalRequest } from '../../../../../hooks'; import type { SendConditionalRequestConfig } from '../../../../../hooks'; @@ -52,37 +51,31 @@ export const usePackagePoliciesWithAgentPolicy = ( resendRequest, } = useGetPackagePolicies(query); - const agentPoliciesFilter = useMemo(() => { + const agentPoliciesIds = useMemo(() => { if (!packagePoliciesData?.items.length) { - return ''; + return []; } // Build a list of package_policies for which we need Agent Policies for. Since some package // policies can exist within the same Agent Policy, we don't need to (in some cases) include // the entire list of package_policy ids. - const includedAgentPolicies = new Set(); - - return `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${packagePoliciesData.items - .filter((packagePolicy) => { - if (includedAgentPolicies.has(packagePolicy.policy_id)) { - return false; - } - includedAgentPolicies.add(packagePolicy.policy_id); - return true; - }) - .map((packagePolicy) => packagePolicy.id) - .join(' or ')}) `; + return Array.from( + new Set( + packagePoliciesData.items.map((packagePolicy) => packagePolicy.policy_id) + ).values() + ); }, [packagePoliciesData]); const { data: agentPoliciesData, isLoading: isLoadingAgentPolicies } = useConditionalRequest({ - path: agentPolicyRouteService.getListPath(), - method: 'get', - query: { - perPage: 100, - kuery: agentPoliciesFilter, + path: agentPolicyRouteService.getBulkGetPath(), + method: 'post', + body: { + ids: agentPoliciesIds, + full: true, + ignoreMissing: true, }, - shouldSendRequest: !!packagePoliciesData?.items.length, + shouldSendRequest: agentPoliciesIds.length > 0, } as SendConditionalRequestConfig); const [enrichedData, setEnrichedData] = useState(); diff --git a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx index 4789770b7046..5a0b6285c71d 100644 --- a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx +++ b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx @@ -52,8 +52,8 @@ export const usePackageInstallations = () => { const updatableIntegrations = useMemo>( () => (agentPolicyData?.items || []).reduce((result, policy) => { - policy.package_policies.forEach((pkgPolicy: PackagePolicy | string) => { - if (typeof pkgPolicy === 'string' || !pkgPolicy.package) return false; + policy.package_policies?.forEach((pkgPolicy: PackagePolicy) => { + if (!pkgPolicy.package) return false; const { name, version } = pkgPolicy.package; const installedPackage = allInstalledPackages.find( (installedPkg) => diff --git a/x-pack/plugins/fleet/public/services/has_fleet_server.ts b/x-pack/plugins/fleet/public/services/has_fleet_server.ts index e1100d6447aa..43724d121b90 100644 --- a/x-pack/plugins/fleet/public/services/has_fleet_server.ts +++ b/x-pack/plugins/fleet/public/services/has_fleet_server.ts @@ -9,8 +9,11 @@ import { FLEET_SERVER_PACKAGE } from '../constants'; import type { AgentPolicy, PackagePolicy } from '../types'; export function policyHasFleetServer(agentPolicy: AgentPolicy) { - return agentPolicy.package_policies?.some( - (ap: string | PackagePolicy) => - typeof ap !== 'string' && ap.package?.name === FLEET_SERVER_PACKAGE + if (!agentPolicy.package_policies) { + return false; + } + + return agentPolicy.package_policies.some( + (ap: PackagePolicy) => ap.package?.name === FLEET_SERVER_PACKAGE ); } diff --git a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts index 30bdf452cde1..b4afbec06787 100644 --- a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts @@ -248,9 +248,7 @@ describe('Fleet preconfiguration reset', () => { it('Works if the preconfigured policies already exists with a missing package policy', async () => { const soClient = kbnServer.coreStart.savedObjects.createInternalRepository(); - await soClient.update('ingest-agent-policies', POLICY_ID, { - package_policies: [], - }); + await soClient.update('ingest-agent-policies', POLICY_ID, {}); const resetAPI = getSupertestWithAdminUser( kbnServer.root, @@ -268,7 +266,6 @@ describe('Fleet preconfiguration reset', () => { expect.arrayContaining([ expect.objectContaining({ name: 'Elastic Cloud agent policy 0001', - package_policies: expect.arrayContaining([expect.stringMatching(/.*/)]), }), expect.objectContaining({ name: 'Second preconfigured policy', diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index b4e5f83d2cb0..a76988506cad 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -121,6 +121,7 @@ export const createPackagePolicyServiceMock = (): jest.Mocked & { - package_configs: string[] | PackagePolicy[]; + Omit & { + package_configs: string[]; + package_policies?: string[]; }, - AgentPolicy + Omit & { + package_policies?: string[]; + } > = (agentPolicyDoc) => { agentPolicyDoc.attributes.package_policies = agentPolicyDoc.attributes.package_configs; // @ts-expect-error diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_5_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_5_0.ts new file mode 100644 index 000000000000..9cbcc85c4693 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_5_0.ts @@ -0,0 +1,22 @@ +/* + * 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 { SavedObjectMigrationFn } from '@kbn/core/server'; + +import type { AgentPolicy } from '../../types'; + +export const migrateAgentPolicyToV850: SavedObjectMigrationFn< + Exclude & { + package_policies: string[]; + }, + AgentPolicy +> = (agentPolicyDoc) => { + // @ts-expect-error + delete agentPolicyDoc.attributes.package_policies; + + return agentPolicyDoc; +}; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts index 0db543e4357a..a60b5c68d05a 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts @@ -31,8 +31,10 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { expect(permissions).toBeUndefined(); }); - it('Throw an error for string package policies', async () => { - await expect(() => storedPackagePoliciesToAgentPermissions(soClient, ['foo'])).rejects.toThrow( + it('Throw an error if package policies is not an array', async () => { + await expect(() => + storedPackagePoliciesToAgentPermissions(soClient, undefined) + ).rejects.toThrow( /storedPackagePoliciesToAgentPermissions should be called with a PackagePolicy/ ); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts index ee17494ca645..b961eb8691f8 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts @@ -20,19 +20,19 @@ export const DEFAULT_CLUSTER_PERMISSIONS = ['monitor']; export async function storedPackagePoliciesToAgentPermissions( soClient: SavedObjectsClientContract, - packagePolicies: string[] | PackagePolicy[] + packagePolicies?: PackagePolicy[] ): Promise { - if (packagePolicies.length === 0) { - return; - } - // I'm not sure what permissions to return for this case, so let's return the defaults - if (typeof packagePolicies[0] === 'string') { + if (!packagePolicies) { throw new Error( 'storedPackagePoliciesToAgentPermissions should be called with a PackagePolicy' ); } + if (packagePolicies.length === 0) { + return; + } + const permissionEntries = (packagePolicies as PackagePolicy[]).map>( async (packagePolicy) => { if (!packagePolicy.package) { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 4d348af47177..8dd8c885af3e 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -75,6 +75,8 @@ const mockedOutputService = outputService as jest.Mocked; const mockedDownloadSourceService = downloadSourceService as jest.Mocked< typeof downloadSourceService >; +const mockedPackagePolicyService = packagePolicyService as jest.Mocked; + const mockedGetFullAgentPolicy = getFullAgentPolicy as jest.Mock< ReturnType >; @@ -144,6 +146,11 @@ describe('agent policy', () => { beforeEach(() => { soClient = getSavedObjectMock({ revision: 1, package_policies: ['package-1'] }); + mockedPackagePolicyService.findAllForAgentPolicy.mockReturnValue([ + { + id: 'package-1', + }, + ] as any); esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; (getAgentsByKuery as jest.Mock).mockResolvedValue({ @@ -153,10 +160,10 @@ describe('agent policy', () => { perPage: 10, }); - (packagePolicyService.delete as jest.Mock).mockResolvedValue([ + mockedPackagePolicyService.delete.mockResolvedValue([ { id: 'package-1', - }, + } as any, ]); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 400810051898..9c70e4a58c38 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { uniq, omit, isEqual, keyBy } from 'lodash'; +import { omit, isEqual, keyBy } from 'lodash'; import uuidv5 from 'uuid/v5'; import { safeDump } from 'js-yaml'; import pMap from 'p-map'; @@ -186,8 +186,9 @@ class AgentPolicyService { } public hasAPMIntegration(agentPolicy: AgentPolicy) { - return agentPolicy.package_policies.some( - (p) => typeof p !== 'string' && p.package?.name === FLEET_APM_PACKAGE + return ( + agentPolicy.package_policies && + agentPolicy.package_policies.some((p) => p.package?.name === FLEET_APM_PACKAGE) ); } @@ -261,13 +262,7 @@ class AgentPolicyService { if (withPackagePolicies) { agentPolicy.package_policies = - (await packagePolicyService.getByIDs( - soClient, - (agentPolicySO.attributes.package_policies as string[]) || [], - { - ignoreMissing: true, - } - )) || []; + (await packagePolicyService.findAllForAgentPolicy(soClient, id)) || []; } return agentPolicy; @@ -458,7 +453,7 @@ class AgentPolicyService { ); // Copy all package policies and append (copy n) to their names - if (baseAgentPolicy.package_policies.length) { + if (baseAgentPolicy.package_policies) { const newPackagePolicies = await pMap( baseAgentPolicy.package_policies as PackagePolicy[], async (packagePolicy: PackagePolicy) => { @@ -610,75 +605,6 @@ class AgentPolicyService { return res; } - public async assignPackagePolicies( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - id: string, - packagePolicyIds: string[], - options: { user?: AuthenticatedUser; bumpRevision: boolean; force?: boolean } = { - bumpRevision: true, - } - ): Promise { - const oldAgentPolicy = await this.get(soClient, id, false); - - if (!oldAgentPolicy) { - throw new Error('Agent policy not found'); - } - - if (oldAgentPolicy.is_managed && !options?.force) { - throw new HostedAgentPolicyRestrictionRelatedError( - `Cannot update integrations of hosted agent policy ${id}` - ); - } - - return await this._update( - soClient, - esClient, - id, - { - package_policies: uniq( - [...((oldAgentPolicy.package_policies || []) as string[])].concat(packagePolicyIds) - ), - }, - options?.user, - { bumpRevision: options.bumpRevision } - ); - } - - public async unassignPackagePolicies( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - id: string, - packagePolicyIds: string[], - options?: { user?: AuthenticatedUser; force?: boolean } - ) { - const oldAgentPolicy = await this.get(soClient, id, false); - - if (!oldAgentPolicy) { - throw new Error('Agent policy not found'); - } - - if (oldAgentPolicy.is_managed && !options?.force) { - throw new HostedAgentPolicyRestrictionRelatedError( - `Cannot remove integrations of hosted agent policy ${id}` - ); - } - - return await this._update( - soClient, - esClient, - id, - { - package_policies: uniq( - [...((oldAgentPolicy.package_policies || []) as string[])].filter( - (packagePolicyId) => !packagePolicyIds.includes(packagePolicyId) - ) - ), - }, - options?.user - ); - } - public async delete( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, @@ -705,12 +631,14 @@ class AgentPolicyService { throw new Error('Cannot delete agent policy that is assigned to agent(s)'); } - if (agentPolicy.package_policies && agentPolicy.package_policies.length) { + const packagePolicies = await packagePolicyService.findAllForAgentPolicy(soClient, id); + + if (packagePolicies.length) { const deletedPackagePolicies: DeletePackagePoliciesResponse = await packagePolicyService.delete( soClient, esClient, - agentPolicy.package_policies as string[], + packagePolicies.map((p) => p.id), { force: options?.force, skipUnassignFromAgentPolicies: true, diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 0e9c2fda047d..f7668f8fe19b 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -55,6 +55,7 @@ import { PackagePolicyValidationError, PackagePolicyRestrictionRelatedError, PackagePolicyNotFoundError, + HostedAgentPolicyRestrictionRelatedError, } from '../errors'; import { NewPackagePolicySchema, PackagePolicySchema, UpdatePackagePolicySchema } from '../types'; import type { @@ -75,7 +76,7 @@ import { outputService } from './output'; import { getPackageInfo, getInstallation, ensureInstalledPackage } from './epm/packages'; import { getAssetsData } from './epm/packages/assets'; import { compileTemplate } from './epm/agent/agent'; -import { normalizeKuery } from './saved_object'; +import { escapeSearchQueryPhrase, normalizeKuery } from './saved_object'; import { appContextService } from '.'; import { removeOldAssets } from './epm/packages/cleanup'; import type { PackageUpdateEvent, UpdateEventType } from './upgrade_sender'; @@ -120,6 +121,7 @@ class PackagePolicyService implements PackagePolicyServiceInterface { throw new IngestManagerError('You cannot add APM to a policy using a logstash output'); } } + await validateIsNotHostedPolicy(soClient, packagePolicy.policy_id, options?.force); // trailing whitespace causes issues creating API keys packagePolicy.name = packagePolicy.name.trim(); @@ -196,18 +198,11 @@ class PackagePolicyService implements PackagePolicyServiceInterface { { ...options, id: packagePolicyId } ); - // Assign it to the given agent policy - await agentPolicyService.assignPackagePolicies( - soClient, - esClient, - packagePolicy.policy_id, - [newSo.id], - { + if (options?.bumpRevision ?? true) { + await agentPolicyService.bumpRevision(soClient, esClient, packagePolicy.policy_id, { user: options?.user, - bumpRevision: options?.bumpRevision ?? true, - force: options?.force, - } - ); + }); + } return { id: newSo.id, @@ -221,8 +216,9 @@ class PackagePolicyService implements PackagePolicyServiceInterface { esClient: ElasticsearchClient, packagePolicies: NewPackagePolicy[], agentPolicyId: string, - options?: { user?: AuthenticatedUser; bumpRevision?: boolean } + options?: { user?: AuthenticatedUser; bumpRevision?: boolean; force: true } ): Promise { + await validateIsNotHostedPolicy(soClient, agentPolicyId); const isoDate = new Date().toISOString(); // eslint-disable-next-line @typescript-eslint/naming-convention const { saved_objects } = await soClient.bulkCreate( @@ -254,16 +250,12 @@ class PackagePolicyService implements PackagePolicyServiceInterface { const newSos = saved_objects.filter((so) => !so.error && so.attributes); // Assign it to the given agent policy - await agentPolicyService.assignPackagePolicies( - soClient, - esClient, - agentPolicyId, - newSos.map((newSo) => newSo.id), - { + + if (options?.bumpRevision ?? true) { + await agentPolicyService.bumpRevision(soClient, esClient, agentPolicyId, { user: options?.user, - bumpRevision: options?.bumpRevision ?? true, - } - ); + }); + } return newSos.map((newSo) => ({ id: newSo.id, @@ -292,6 +284,26 @@ class PackagePolicyService implements PackagePolicyServiceInterface { }; } + public async findAllForAgentPolicy( + soClient: SavedObjectsClientContract, + agentPolicyId: string + ): Promise { + const packagePolicySO = await soClient.find({ + type: SAVED_OBJECT_TYPE, + filter: `${SAVED_OBJECT_TYPE}.attributes.policy_id:${escapeSearchQueryPhrase(agentPolicyId)}`, + perPage: SO_SEARCH_LIMIT, + }); + if (!packagePolicySO) { + return []; + } + + return packagePolicySO.saved_objects.map((so) => ({ + id: so.id, + version: so.version, + ...so.attributes, + })); + } + public async getByIDs( soClient: SavedObjectsClientContract, ids: string[], @@ -501,6 +513,13 @@ class PackagePolicyService implements PackagePolicyServiceInterface { throw new PackagePolicyRestrictionRelatedError(`Cannot delete package policy ${id}`); } + await validateIsNotHostedPolicy( + soClient, + packagePolicy?.policy_id, + options?.force, + 'Cannot remove integrations of hosted agent policy' + ); + const agentPolicy = await agentPolicyService .get(soClient, packagePolicy.policy_id) .catch((err) => { @@ -513,19 +532,12 @@ class PackagePolicyService implements PackagePolicyServiceInterface { throw err; }); + await soClient.delete(SAVED_OBJECT_TYPE, id); if (agentPolicy && !options?.skipUnassignFromAgentPolicies) { - await agentPolicyService.unassignPackagePolicies( - soClient, - esClient, - packagePolicy.policy_id, - [packagePolicy.id], - { - user: options?.user, - force: options?.force, - } - ); + await agentPolicyService.bumpRevision(soClient, esClient, packagePolicy.policy_id, { + user: options?.user, + }); } - await soClient.delete(SAVED_OBJECT_TYPE, id); result.push({ id, name: packagePolicy.name, @@ -1281,6 +1293,11 @@ export interface PackagePolicyServiceInterface { get(soClient: SavedObjectsClientContract, id: string): Promise; + findAllForAgentPolicy( + soClient: SavedObjectsClientContract, + agentPolicyId: string + ): Promise; + getByIDs( soClient: SavedObjectsClientContract, ids: string[], @@ -1607,6 +1624,25 @@ export function preconfigurePackageInputs( return resultingPackagePolicy; } +async function validateIsNotHostedPolicy( + soClient: SavedObjectsClientContract, + id: string, + force = false, + errorMessage?: string +) { + const agentPolicy = await agentPolicyService.get(soClient, id, false); + + if (!agentPolicy) { + throw new Error('Agent policy not found'); + } + + if (agentPolicy.is_managed && !force) { + throw new HostedAgentPolicyRestrictionRelatedError( + errorMessage ?? `Cannot update integrations of hosted agent policy ${id}` + ); + } +} + function deepMergeVars(original: any, override: any, keepOriginalValue = false): any { if (!original.vars) { original.vars = { ...override.vars }; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index c0b24f927d84..245ab2316cb0 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -238,7 +238,8 @@ jest.mock('./epm/kibana/index_pattern/install'); jest.mock('./package_policy', () => ({ ...jest.requireActual('./package_policy'), packagePolicyService: { - getByIDs: jest.fn().mockReturnValue([]), + ...jest.requireActual('./package_policy').packagePolicyService, + findAllForAgentPolicy: jest.fn().mockReturnValue([]), listIds: jest.fn().mockReturnValue({ items: [] }), create: jest .fn() @@ -280,8 +281,8 @@ const spyAgentPolicyServicBumpAllAgentPoliciesForOutput = jest.spyOn( describe('policy preconfiguration', () => { beforeEach(() => { - mockedPackagePolicyService.getByIDs.mockReset(); mockedPackagePolicyService.create.mockReset(); + mockedPackagePolicyService.findAllForAgentPolicy.mockReset(); mockInstalledPackages.clear(); mockInstallPackageErrors.clear(); mockConfiguredPolicies.clear(); @@ -365,7 +366,7 @@ describe('policy preconfiguration', () => { it('should not add new package policy to existing non managed policies', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - mockedPackagePolicyService.getByIDs.mockResolvedValue([ + mockedPackagePolicyService.findAllForAgentPolicy.mockResolvedValue([ { name: 'test_package1' } as PackagePolicy, ]); @@ -415,7 +416,7 @@ describe('policy preconfiguration', () => { it('should add new package policy to existing managed policies', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - mockedPackagePolicyService.getByIDs.mockResolvedValue([ + mockedPackagePolicyService.findAllForAgentPolicy.mockResolvedValue([ { name: 'test_package1' } as PackagePolicy, ]); @@ -475,7 +476,7 @@ describe('policy preconfiguration', () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - mockedPackagePolicyService.getByIDs.mockResolvedValue([ + mockedPackagePolicyService.findAllForAgentPolicy.mockResolvedValue([ { name: 'Renamed package policy', id: 'test_package1' } as PackagePolicy, ]); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_policy_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_policy_generator.ts index 82294ac754fc..5b99a28e96ee 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_policy_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_policy_generator.ts @@ -22,7 +22,6 @@ export class FleetAgentPolicyGenerator extends BaseDataGenerator { - return agentPolicyGenerator.generate({ - package_policies: [packagePolicyId], + return agentPolicyGenerator.generate({}); + }), + perPage: Math.max(requiredPolicyIds.length, 10), + total: requiredPolicyIds.length, + page: 1, + }; + }, + }, + ]); + +export type FleetBulkGetAgentPolicyListHttpMockInterface = ResponseProvidersInterface<{ + agentPolicy: () => BulkGetAgentPoliciesResponse; +}>; +export const fleetBulkGetAgentPolicyListHttpMock = + httpHandlerMockFactory([ + { + id: 'agentPolicy', + path: AGENT_POLICY_API_ROUTES.BULK_GET_PATTERN, + method: 'post', + handler: ({ body }) => { + const generator = new EndpointDocGenerator('seed'); + const agentPolicyGenerator = new FleetAgentPolicyGenerator('seed'); + const endpointMetadata = generator.generateHostMetadata(); + const requiredPolicyIds: string[] = [ + // Make sure that the Agent policy returned from the API has the Integration Policy ID that + // the first endpoint metadata generated is using. This is needed especially when testing the + // Endpoint Details flyout where certain actions might be disabled if we know the endpoint integration policy no + // longer exists. + endpointMetadata.Endpoint.policy.applied.id, + + // In addition, some of our UI logic looks for the existence of certain Endpoint Integration policies + // using the Agents Policy API (normally when checking IDs since query by ids is not supported via API) + // so also add the first two package policy IDs that the `fleetGetEndpointPackagePolicyListHttpMock()` + // method above creates (which Trusted Apps HTTP mocks also use) + // FIXME: remove hard-coded IDs below and get them from the new FleetPackagePolicyGenerator (#2262) + 'ddf6570b-9175-4a6d-b288-61a09771c647', + 'b8e616ae-44fc-4be7-846c-ce8fa5c082dd', + ]; + + return { + items: requiredPolicyIds.map((packagePolicyId) => { + return agentPolicyGenerator.generate({}); + }), + }; + }, + }, + ]); + +export type FleetBulkGetPackagePoliciesListHttpMockInterface = ResponseProvidersInterface<{ + packagePolicies: () => BulkGetPackagePoliciesResponse; +}>; +export const fleetBulkGetPackagePoliciesListHttpMock = + httpHandlerMockFactory([ + { + id: 'packagePolicies', + path: PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN, + method: 'post', + handler: ({ body }) => { + const generator = new EndpointDocGenerator('seed'); + const fleetPackagePolicyGenerator = new FleetPackagePolicyGenerator('seed'); + const endpointMetadata = generator.generateHostMetadata(); + const requiredPolicyIds: string[] = [ + // Make sure that the Agent policy returned from the API has the Integration Policy ID that + // the first endpoint metadata generated is using. This is needed especially when testing the + // Endpoint Details flyout where certain actions might be disabled if we know the endpoint integration policy no + // longer exists. + endpointMetadata.Endpoint.policy.applied.id, + + // In addition, some of our UI logic looks for the existence of certain Endpoint Integration policies + // using the Agents Policy API (normally when checking IDs since query by ids is not supported via API) + // so also add the first two package policy IDs that the `fleetGetEndpointPackagePolicyListHttpMock()` + // method above creates (which Trusted Apps HTTP mocks also use) + // FIXME: remove hard-coded IDs below and get them from the new FleetPackagePolicyGenerator (#2262) + 'ddf6570b-9175-4a6d-b288-61a09771c647', + 'b8e616ae-44fc-4be7-846c-ce8fa5c082dd', + + // And finally, include any kql filters for package policies ids + ...getPackagePoliciesFromKueryString( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${( + JSON.parse(body?.toString() ?? '{}')?.ids as string[] + ).join(' or ')} )` + ), + ]; + + return { + items: requiredPolicyIds.map((packagePolicyId) => { + return fleetPackagePolicyGenerator.generate({ + id: packagePolicyId, + }); + }), + }; + }, + }, + ]); + +export type FleetGetPackagePoliciesListHttpMockInterface = ResponseProvidersInterface<{ + packagePolicies: () => GetPackagePoliciesResponse; +}>; +export const fleetGetPackagePoliciesListHttpMock = + httpHandlerMockFactory([ + { + id: 'packagePolicies', + path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, + method: 'get', + handler: ({ query }) => { + const generator = new EndpointDocGenerator('seed'); + const fleetPackagePolicyGenerator = new FleetPackagePolicyGenerator('seed'); + const endpointMetadata = generator.generateHostMetadata(); + const requiredPolicyIds: string[] = [ + // Make sure that the Agent policy returned from the API has the Integration Policy ID that + // the first endpoint metadata generated is using. This is needed especially when testing the + // Endpoint Details flyout where certain actions might be disabled if we know the endpoint integration policy no + // longer exists. + endpointMetadata.Endpoint.policy.applied.id, + + // In addition, some of our UI logic looks for the existence of certain Endpoint Integration policies + // using the Agents Policy API (normally when checking IDs since query by ids is not supported via API) + // so also add the first two package policy IDs that the `fleetGetEndpointPackagePolicyListHttpMock()` + // method above creates (which Trusted Apps HTTP mocks also use) + // FIXME: remove hard-coded IDs below and get them from the new FleetPackagePolicyGenerator (#2262) + 'ddf6570b-9175-4a6d-b288-61a09771c647', + 'b8e616ae-44fc-4be7-846c-ce8fa5c082dd', + + // And finally, include any kql filters for package policies ids + ...getPackagePoliciesFromKueryString( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${(query?.ids as string[]).join( + ' or ' + )} )` + ), + ]; + + return { + items: requiredPolicyIds.map((packagePolicyId) => { + return fleetPackagePolicyGenerator.generate({ + id: packagePolicyId, }); }), perPage: Math.max(requiredPolicyIds.length, 10), diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index 4cc923cf8d86..7dfd55664acc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -38,6 +38,9 @@ import { fleetGetAgentPolicyListHttpMock, fleetGetCheckPermissionsHttpMock, fleetGetPackageListHttpMock, + fleetBulkGetPackagePoliciesListHttpMock, + fleetBulkGetAgentPolicyListHttpMock, + fleetGetPackagePoliciesListHttpMock, } from '../../mocks'; type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{ @@ -133,6 +136,9 @@ export const endpointListFleetApisHttpMock = composeHttpHandlerMocks([ fleetGetPackageListHttpMock, fleetGetAgentPolicyListHttpMock, + fleetBulkGetPackagePoliciesListHttpMock, + fleetBulkGetAgentPolicyListHttpMock, + fleetGetPackagePoliciesListHttpMock, fleetGetCheckPermissionsHttpMock, ]); type EndpointPageHttpMockInterface = EndpointMetadataHttpMocksInterface & diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 70b31f88b2f7..7d1fd0a3d77f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -50,6 +50,7 @@ import { jest.mock('../../../services/policies/ingest', () => ({ sendGetAgentConfigList: () => Promise.resolve({ items: [] }), sendGetAgentPolicyList: () => Promise.resolve({ items: [] }), + sendBulkGetPackagePolicies: () => Promise.resolve({ items: [] }), sendGetEndpointSecurityPackage: () => Promise.resolve({ version: '1.1.1' }), sendGetFleetAgentsWithEndpoint: () => Promise.resolve({ total: 0 }), })); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 2194e51ec5aa..fb0e1949ad75 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -9,7 +9,6 @@ import type { DataViewBase, Query } from '@kbn/es-query'; import type { CoreStart, HttpStart } from '@kbn/core/public'; import type { Dispatch } from 'redux'; import semverGte from 'semver/functions/gte'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; import { BASE_POLICY_RESPONSE_ROUTE, HOST_METADATA_GET_ROUTE, @@ -41,7 +40,7 @@ import { createLoadingResourceState, } from '../../../state'; import { - sendGetAgentPolicyList, + sendBulkGetPackagePolicies, sendGetEndpointSecurityPackage, sendGetFleetAgentsWithEndpoint, } from '../../../services/policies/ingest'; @@ -173,19 +172,12 @@ const getAgentAndPoliciesForEndpointsList = async ( // Package Ids that it uses, thus if a reference exists there, then the package policy (policy) // exists. const policiesFound = ( - await sendGetAgentPolicyList(http, { - query: { - kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${policyIdsToCheck.join( - ' or ' - )})`, - }, - }) + await sendBulkGetPackagePolicies(http, policyIdsToCheck) ).items.reduce( - (list, agentPolicy) => { - (agentPolicy.package_policies as string[]).forEach((packagePolicy) => { - list.packagePolicy[packagePolicy as string] = true; - list.agentPolicy[packagePolicy as string] = agentPolicy.id; - }); + (list, packagePolicy) => { + list.packagePolicy[packagePolicy.id as string] = true; + list.agentPolicy[packagePolicy.id as string] = packagePolicy.policy_id; + return list; }, { packagePolicy: {}, agentPolicy: {} } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index 03a0fdda8979..5a983574f754 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -11,6 +11,7 @@ import type { GetAgentPoliciesResponseItem, GetPackagesResponse, GetAgentsResponse, + BulkGetPackagePoliciesResponse, } from '@kbn/fleet-plugin/common/types/rest_spec'; import type { GetHostPolicyResponse, @@ -120,9 +121,6 @@ const endpointListApiPathHandlerMocks = ({ // Do policies referenced in endpoint list exist // just returns 1 single agent policy that includes all of the packagePolicy IDs provided [INGEST_API_AGENT_POLICIES]: (): GetAgentPoliciesResponse => { - (agentPolicy.package_policies as string[]).push( - ...endpointPackagePolicies.map((packagePolicy) => packagePolicy.id) - ); return { items: [agentPolicy], total: 10, @@ -146,6 +144,13 @@ const endpointListApiPathHandlerMocks = ({ }; }, + // List of Policies (package policies) for onboarding + [`${INGEST_API_PACKAGE_POLICIES}/_bulk_get`]: (): BulkGetPackagePoliciesResponse => { + return { + items: endpointPackagePolicies, + }; + }, + // List of Agents using Endpoint [INGEST_API_FLEET_AGENTS]: (): GetAgentsResponse => { return { @@ -209,4 +214,15 @@ export const setEndpointListApiMockImplementation: ( throw new Error(`MOCK: api request does not have a mocked handler: ${path}`); }); + + mockedHttpService.post.mockImplementation(async (...args) => { + const [path] = args; + if (typeof path === 'string') { + if (apiHandlers[path]) { + return apiHandlers[path](); + } + } + + throw new Error(`MOCK: api request does not have a mocked handler: ${path}`); + }); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 132b5684fa8c..1d47f70227df 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -1066,9 +1066,11 @@ describe('when on the endpoint list page', () => { const packagePolicy = docGenerator.generatePolicyPackagePolicy(); packagePolicy.id = hosts[0].metadata.Endpoint.policy.applied.id; + const agentPolicy = generator.generateAgentPolicy(); agentPolicyId = agentPolicy.id; agentId = hosts[0].metadata.elastic.agent.id; + packagePolicy.policy_id = agentPolicyId; setEndpointListApiMockImplementation(coreStart.http, { endpointsResults: hostInfo, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index 66137aab4cea..5b74d99d6abc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -12,7 +12,7 @@ import { createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { sendGetEndpointSpecificPackagePolicies } from '../../../services/policies/policies'; import { sendGetEndpointSpecificPackagePoliciesMock } from '../../../services/policies/test_mock_utils'; import { PolicyList } from './policy_list'; -import { sendGetAgentPolicyList } from '../../../services/policies/ingest'; +import { sendBulkGetAgentPolicyList } from '../../../services/policies/ingest'; import type { GetPolicyListResponse } from '../types'; import { getEndpointListPath, getPoliciesPath } from '../../../common/routing'; import { APP_UI_ID } from '../../../../../common/constants'; @@ -22,7 +22,7 @@ jest.mock('../../../services/policies/ingest'); const getPackagePolicies = sendGetEndpointSpecificPackagePolicies as jest.Mock; -const getAgentPolicies = sendGetAgentPolicyList as jest.Mock; +const mockedSendBulkGetAgentPolicies = sendBulkGetAgentPolicyList as jest.Mock; describe('When on the policy list page', () => { let render: () => ReturnType; @@ -77,19 +77,19 @@ describe('When on the policy list page', () => { beforeEach(async () => { getPackagePolicies.mockReturnValue(policies); - getAgentPolicies.mockReturnValue({ + mockedSendBulkGetAgentPolicies.mockReturnValue({ items: [ - { package_policies: [policies.items[0].id], agents: 4 }, - { package_policies: [policies.items[1].id], agents: 2 }, - { package_policies: [policies.items[2].id], agents: 5 }, - { package_policies: [policies.items[3].id], agents: 1 }, - { package_policies: [policies.items[4].id], agents: 3 }, + { package_policies: [{ id: policies.items[0].id }], agents: 4 }, + { package_policies: [{ id: policies.items[1].id }], agents: 2 }, + { package_policies: [{ id: policies.items[2].id }], agents: 5 }, + { package_policies: [{ id: policies.items[3].id }], agents: 1 }, + { package_policies: [{ id: policies.items[4].id }], agents: 3 }, ], }); render(); await waitFor(() => { expect(sendGetEndpointSpecificPackagePolicies).toHaveBeenCalled(); - expect(sendGetAgentPolicyList).toHaveBeenCalled(); + expect(sendBulkGetAgentPolicyList).toHaveBeenCalled(); }); }); it('should display the policy list table', () => { @@ -164,7 +164,7 @@ describe('When on the policy list page', () => { await waitFor(() => { expect(getPackagePolicies).toHaveBeenCalled(); expect(sendGetEndpointSpecificPackagePolicies).toHaveBeenCalled(); - expect(sendGetAgentPolicyList).toHaveBeenCalled(); + expect(mockedSendBulkGetAgentPolicies).toHaveBeenCalled(); }); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 43a7223260f2..33fd25d0d15c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -19,7 +19,6 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { useLocation } from 'react-router-dom'; -import type { AgentPolicy } from '@kbn/fleet-plugin/common'; import type { CreatePackagePolicyRouteState } from '@kbn/fleet-plugin/public'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; import { AdministrationListPage } from '../../../components/administration_list_page'; @@ -58,10 +57,14 @@ export const PolicyList = memo(() => { // endpoint count per policy const policyIds = useMemo(() => data?.items.map((policies) => policies.id) ?? [], [data]); + const agentPolicyIds = useMemo( + () => data?.items.map((policies) => policies.policy_id) ?? [], + [data] + ); const { data: endpointCount = { items: [] } } = useGetAgentCountForPolicy({ - policyIds, + agentPolicyIds, customQueryOptions: { - enabled: policyIds.length > 0, + enabled: agentPolicyIds.length > 0, onError: (err) => { toasts.addDanger( i18n.translate('xpack.securitySolution.policyList.endpointCountError', { @@ -76,7 +79,7 @@ export const PolicyList = memo(() => { const { data: endpointPackageInfo, isFetching: packageIsFetching } = useGetEndpointSecurityPackage({ customQueryOptions: { - enabled: policyIds.length === 0, + enabled: agentPolicyIds.length === 0, onError: (err) => { toasts.addDanger( i18n.translate('xpack.securitySolution.policyList.packageVersionError', { @@ -88,11 +91,14 @@ export const PolicyList = memo(() => { }); const policyIdToEndpointCount = useMemo(() => { - const map = new Map(); - for (const policy of endpointCount?.items) { - for (const packagePolicyId of policy.package_policies) { - if (policyIds.includes(packagePolicyId as string)) { - map.set(packagePolicyId, policy.agents ?? 0); + const map = new Map(); + + for (const agentPolicy of endpointCount?.items) { + if (agentPolicy.package_policies) { + for (const packagePolicy of agentPolicy.package_policies) { + if (policyIds.includes(packagePolicy.id)) { + map.set(packagePolicy.id, agentPolicy.agents ?? 0); + } } } } diff --git a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts index fef45b657d5d..3c0810b0d551 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts @@ -8,10 +8,9 @@ import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query import { useQuery } from '@tanstack/react-query'; import type { IHttpFetchError } from '@kbn/core-http-browser'; import type { GetAgentPoliciesResponse, GetPackagesResponse } from '@kbn/fleet-plugin/common'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; import { useHttp } from '../../../common/lib/kibana'; import { MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../common/constants'; -import { sendGetAgentPolicyList, sendGetEndpointSecurityPackage } from './ingest'; +import { sendBulkGetAgentPolicyList, sendGetEndpointSecurityPackage } from './ingest'; import type { GetPolicyListResponse } from '../../pages/policy/types'; import { sendGetEndpointSpecificPackagePolicies } from './policies'; import type { ServerApiError } from '../../../common/types'; @@ -53,22 +52,17 @@ export function useGetEndpointSpecificPolicies( * This hook returns the fleet agent policies list filtered by policy id */ export function useGetAgentCountForPolicy({ - policyIds, + agentPolicyIds, customQueryOptions, }: { - policyIds: string[]; + agentPolicyIds: string[]; customQueryOptions?: UseQueryOptions; }): QueryObserverResult { const http = useHttp(); return useQuery( - ['endpointCountForPolicy', policyIds], + ['endpointCountForPolicy', agentPolicyIds], () => { - return sendGetAgentPolicyList(http, { - query: { - perPage: 50, - kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${policyIds.join(' or ')})`, - }, - }); + return sendBulkGetAgentPolicyList(http, agentPolicyIds); }, customQueryOptions ); diff --git a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts index b77ab8a725ec..7690348391ad 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts @@ -12,6 +12,7 @@ import type { GetPackagesResponse, GetAgentPoliciesRequest, GetAgentPoliciesResponse, + GetPackagePoliciesResponse, } from '@kbn/fleet-plugin/common'; import type { NewPolicyData } from '../../../../common/endpoint/types'; import type { GetPolicyResponse, UpdatePolicyResponse } from '../../pages/policy/types'; @@ -37,6 +38,26 @@ export const sendGetPackagePolicy = ( return http.get(`${INGEST_API_PACKAGE_POLICIES}/${packagePolicyId}`, options); }; +/** + * Retrieves multiple package policies by ids + * @param http + * @param packagePolicyIds + * @param options + */ +export const sendBulkGetPackagePolicies = ( + http: HttpStart, + packagePolicyIds: string[], + options?: HttpFetchOptions +) => { + return http.post(`${INGEST_API_PACKAGE_POLICIES}/_bulk_get`, { + ...options, + body: JSON.stringify({ + ids: packagePolicyIds, + ignoreMissing: true, + }), + }); +}; + /** * Retrieve a list of Agent Policies * @param http @@ -49,6 +70,26 @@ export const sendGetAgentPolicyList = ( return http.get(INGEST_API_AGENT_POLICIES, options); }; +/** + * Retrieve a list of Agent Policies + * @param http + * @param options + */ +export const sendBulkGetAgentPolicyList = ( + http: HttpStart, + ids: string[], + options: HttpFetchOptions = {} +) => { + return http.post(`${INGEST_API_AGENT_POLICIES}/_bulk_get`, { + ...options, + body: JSON.stringify({ + ids, + ignoreMissing: true, + full: true, + }), + }); +}; + /** * Updates a package policy * diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts index fc95c7a02539..d99b65943985 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts @@ -64,14 +64,7 @@ type AgentPolicyWithPackagePolicies = Omit & { const isAgentPolicyWithPackagePolicies = ( agentPolicy: AgentPolicy | AgentPolicyWithPackagePolicies ): agentPolicy is AgentPolicyWithPackagePolicies => { - if ( - agentPolicy.package_policies.length === 0 || - typeof agentPolicy.package_policies[0] !== 'string' - ) { - return true; - } - - return false; + return agentPolicy.package_policies ? true : false; }; export class EndpointMetadataService {