From 9cd2cfa861713ab4dd8351794e3abfdbc7807ca1 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Tue, 24 Sep 2024 12:08:54 +0200 Subject: [PATCH] [EDR Workflows][Fleet] Accurate endpoint count across multiple agent policies (#193705) This PR updates the method for counting endpoint statuses. Previously, we fetched agent status using a single agent policy ID. With this change, we now pass an array of policy IDs, allowing us to include the returned stats for endpoints that share the same integration policy assigned to multiple agent policies. ![Screenshot 2024-09-23 at 13 53 57](https://github.com/user-attachments/assets/570027b7-79d7-4c9a-aa64-c0ecfe76cb7f) ![Screenshot 2024-09-23 at 13 53 24](https://github.com/user-attachments/assets/17d62c24-9d46-4133-a817-ea5849930435) ![Screenshot 2024-09-23 at 13 53 45](https://github.com/user-attachments/assets/c9fb5ed7-e4a0-4faa-a24d-253def10f163) --- .../fleet/server/routes/agent/handlers.ts | 3 +- .../server/services/agents/status.test.ts | 65 +++++++++++++++++++ .../fleet/server/services/agents/status.ts | 23 ++++++- .../fleet/server/types/rest_spec/agent.ts | 1 + .../middleware/policy_settings_middleware.ts | 2 +- .../management/services/policies/ingest.ts | 6 +- 6 files changed, 92 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index e328c73878980..67703bba5caae 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -328,7 +328,8 @@ export const getAgentStatusForAgentPolicyHandler: FleetRequestHandler< soClient, request.query.policyId, request.query.kuery, - coreContext.savedObjects.client.getCurrentNamespace() + coreContext.savedObjects.client.getCurrentNamespace(), + request.query.policyIds ); const body: GetAgentStatusResponse = { results }; diff --git a/x-pack/plugins/fleet/server/services/agents/status.test.ts b/x-pack/plugins/fleet/server/services/agents/status.test.ts index 346352f26de70..ae78985a40a1f 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.test.ts @@ -7,6 +7,8 @@ import { errors as EsErrors } from '@elastic/elasticsearch'; +import { AGENTS_INDEX } from '../../../common'; + import { createAppContextStartContractMock } from '../../mocks'; import { appContextService } from '../app_context'; @@ -168,4 +170,67 @@ describe('getAgentStatusForAgentPolicy', () => { expect(esClient.search).toHaveBeenCalledTimes(2); }); + + it('calls esClient.search with correct parameters when agentPolicyIds are provided', async () => { + const esClient = { + search: jest.fn().mockResolvedValue({ + aggregations: { + status: { + buckets: [ + { key: 'online', doc_count: 2 }, + { key: 'error', doc_count: 1 }, + ], + }, + }, + }), + }; + + const soClient = { + find: jest.fn().mockResolvedValue({ + saved_objects: [ + { id: 'agentPolicyId1', attributes: { name: 'Policy 1' } }, + { id: 'agentPolicyId2', attributes: { name: 'Policy 2' } }, + ], + }), + }; + + const agentPolicyIds = ['agentPolicyId1', 'agentPolicyId2']; + const filterKuery = 'filterKuery'; + const spaceId = 'spaceId'; + + await getAgentStatusForAgentPolicy( + esClient as any, + soClient as any, + undefined, + filterKuery, + spaceId, + agentPolicyIds + ); + + expect(esClient.search).toHaveBeenCalledWith( + expect.objectContaining({ + index: AGENTS_INDEX, + size: 0, + query: expect.objectContaining({ + bool: expect.objectContaining({ + must: expect.arrayContaining([ + expect.objectContaining({ + terms: { + policy_id: agentPolicyIds, + }, + }), + ]), + }), + }), + aggregations: expect.objectContaining({ + status: expect.objectContaining({ + terms: expect.objectContaining({ + field: 'status', + size: expect.any(Number), + }), + }), + }), + }) + ); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 99d2d25b139c8..c413ee7e268c8 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -40,12 +40,23 @@ export async function getAgentStatusById( return (await getAgentById(esClient, soClient, agentId)).status!; } +/** + * getAgentStatusForAgentPolicy + * @param esClient + * @param soClient + * @param agentPolicyId @deprecated use agentPolicyIds instead since the move to multi-policy + * @param filterKuery + * @param spaceId + * @param agentPolicyIds + */ + export async function getAgentStatusForAgentPolicy( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, agentPolicyId?: string, filterKuery?: string, - spaceId?: string + spaceId?: string, + agentPolicyIds?: string[] ) { const logger = appContextService.getLogger(); const runtimeFields = await buildAgentStatusRuntimeField(soClient); @@ -71,8 +82,14 @@ export async function getAgentStatusForAgentPolicy( ); clauses.push(kueryAsElasticsearchQuery); } - - if (agentPolicyId) { + // If agentPolicyIds is provided, we filter by those, otherwise we filter by depreciated agentPolicyId + if (agentPolicyIds) { + clauses.push({ + terms: { + policy_id: agentPolicyIds, + }, + }); + } else if (agentPolicyId) { clauses.push({ term: { policy_id: agentPolicyId, diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index 4bbd065e23003..82cae68602e94 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -241,6 +241,7 @@ export const PostBulkUpdateAgentTagsRequestSchema = { export const GetAgentStatusRequestSchema = { query: schema.object({ policyId: schema.maybe(schema.string()), + policyIds: schema.maybe(schema.arrayOf(schema.string())), kuery: schema.maybe( schema.string({ validate: (value: string) => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts index a22dd2a5edfdf..e74403777523c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts @@ -97,7 +97,7 @@ export const policySettingsMiddlewareRunner: MiddlewareRunner = async ( // Agent summary is secondary data, so its ok for it to come after the details // page is populated with the main content if (policyItem.policy_id) { - const { results } = await sendGetFleetAgentStatusForPolicy(http, policyItem.policy_id); + const { results } = await sendGetFleetAgentStatusForPolicy(http, policyItem.policy_ids); dispatch({ type: 'serverReturnedPolicyDetailsAgentSummaryData', payload: { 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 4a2690a112e24..523b1b9a858b1 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 @@ -86,20 +86,20 @@ export const sendPutPackagePolicy = ( * Get a status summary for all Agents that are currently assigned to a given agent policy * * @param http - * @param policyId + * @param policyIds * @param options */ export const sendGetFleetAgentStatusForPolicy = ( http: HttpStart, /** the Agent (fleet) policy id */ - policyId: string, + policyIds: string[], options: Exclude = {} ): Promise => { return http.get(INGEST_API_FLEET_AGENT_STATUS, { ...options, version: API_VERSIONS.public.v1, query: { - policyId, + policyIds, }, }); };