diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 984cd7d2506a..e311e358e614 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -7,6 +7,5 @@ export const eventsIndexPattern = 'logs-endpoint.events.*'; export const alertsIndexPattern = 'logs-endpoint.alerts-*'; export const metadataIndexPattern = 'metrics-endpoint.metadata-*'; -export const metadataMirrorIndexPattern = 'metrics-endpoint.metadata_mirror-*'; export const policyIndexPattern = 'metrics-endpoint.policy-*'; export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 7c50a10846f9..235e7152b83c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -18,8 +18,8 @@ import { HostStatus, } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; -import { AgentStatus } from '../../../../../ingest_manager/common/types/models'; -import { findAllUnenrolledHostIds, findUnenrolledHostByHostId, HostId } from './support/unenroll'; +import { Agent, AgentStatus } from '../../../../../ingest_manager/common/types/models'; +import { findAllUnenrolledAgentIds } from './support/unenroll'; interface HitSource { _source: HostMetadata; @@ -70,8 +70,9 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const unenrolledHostIds = await findAllUnenrolledHostIds( - context.core.elasticsearch.legacy.client + const unenrolledAgentIds = await findAllUnenrolledAgentIds( + endpointAppContext.service.getAgentService(), + context.core.savedObjects.client ); const queryParams = await kibanaRequestToMetadataListESQuery( @@ -79,9 +80,10 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp endpointAppContext, metadataIndexPattern, { - unenrolledHostIds: unenrolledHostIds.map((host: HostId) => host.host.id), + unenrolledAgentIds, } ); + const response = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( 'search', queryParams @@ -138,13 +140,6 @@ export async function getHostData( metadataRequestContext: MetadataRequestContext, id: string ): Promise { - const unenrolledHostId = await findUnenrolledHostByHostId( - metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client, - id - ); - if (unenrolledHostId) { - throw Boom.badRequest('the requested endpoint is unenrolled'); - } const query = getESQueryHostMetadataByID(id, metadataIndexPattern); const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client.callAsCurrentUser( 'search', @@ -155,7 +150,36 @@ export async function getHostData( return undefined; } - return enrichHostMetadata(response.hits.hits[0]._source, metadataRequestContext); + const hostMetadata: HostMetadata = response.hits.hits[0]._source; + const agent = await findAgent(metadataRequestContext, hostMetadata); + + if (agent && !agent.active) { + throw Boom.badRequest('the requested endpoint is unenrolled'); + } + + return enrichHostMetadata(hostMetadata, metadataRequestContext); +} + +async function findAgent( + metadataRequestContext: MetadataRequestContext, + hostMetadata: HostMetadata +): Promise { + const logger = metadataRequestContext.endpointAppContext.logFactory.get('metadata'); + try { + return await metadataRequestContext.endpointAppContext.service + .getAgentService() + .getAgent( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + hostMetadata.elastic.agent.id + ); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + logger.warn(`agent with id ${hostMetadata.elastic.agent.id} not found`); + return undefined; + } else { + throw e; + } + } } async function mapToHostResultList( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index f6ae2c584a34..668911b8d1f2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -35,7 +35,7 @@ import Boom from 'boom'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; -import { HostId } from './support/unenroll'; +import { Agent } from '../../../../../ingest_manager/common/types/models'; describe('test endpoint route', () => { let routerMock: jest.Mocked; @@ -51,12 +51,12 @@ describe('test endpoint route', () => { typeof createMockEndpointAppContextServiceStartContract >['agentService']; let endpointAppContextService: EndpointAppContextService; - const noUnenrolledEndpoint = () => - Promise.resolve(({ - hits: { - hits: [], - }, - } as unknown) as SearchResponse); + const noUnenrolledAgent = { + agents: [], + total: 0, + page: 1, + perPage: 1, + }; beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked< @@ -84,20 +84,19 @@ describe('test endpoint route', () => { it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); const response = createSearchResponse(new EndpointDocGenerator().generateHostMetadata()); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => Promise.resolve(response)); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); + mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; @@ -122,11 +121,10 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => - Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) - ); + mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) + ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -137,8 +135,8 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); - expect(mockScopedClient.callAsCurrentUser.mock.calls[1][1]?.body?.query).toEqual({ + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({ match_all: {}, }); expect(routeConfig.options).toEqual({ authRequired: true }); @@ -167,11 +165,10 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => - Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) - ); + mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) + ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -183,7 +180,7 @@ describe('test endpoint route', () => { ); expect(mockScopedClient.callAsCurrentUser).toBeCalled(); - expect(mockScopedClient.callAsCurrentUser.mock.calls[1][1]?.body?.query).toEqual({ + expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({ bool: { must: [ { @@ -218,11 +215,15 @@ describe('test endpoint route', () => { it('should return 404 on no results', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => Promise.resolve(createSearchResponse())); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createSearchResponse()) + ); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); + mockAgentService.getAgent = jest.fn().mockReturnValue(({ + active: true, + } as unknown) as Agent); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -232,7 +233,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.notFound).toBeCalled(); const message = mockResponse.notFound.mock.calls[0][0]?.body; @@ -246,9 +247,10 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => Promise.resolve(response)); + mockAgentService.getAgent = jest.fn().mockReturnValue(({ + active: true, + } as unknown) as Agent); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -260,7 +262,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; @@ -279,9 +281,11 @@ describe('test endpoint route', () => { throw Boom.notFound('Agent not found'); }); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => Promise.resolve(response)); + mockAgentService.getAgent = jest.fn().mockImplementation(() => { + throw Boom.notFound('Agent not found'); + }); + + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -293,7 +297,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; @@ -308,10 +312,10 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning'); - - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => Promise.resolve(response)); + mockAgentService.getAgent = jest.fn().mockReturnValue(({ + active: true, + } as unknown) as Agent); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -323,36 +327,23 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result.host_status).toEqual(HostStatus.ERROR); }); - it('should throw error when endpoint is unenrolled', async () => { + it('should throw error when endpoint egent is not active', async () => { + const response = createSearchResponse(new EndpointDocGenerator().generateHostMetadata()); + const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: 'hostId' }, + params: { id: response.hits.hits[0]._id }, }); - - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(({ - hits: { - hits: [ - { - _index: 'metrics-endpoint.metadata_mirror-default', - _id: 'S5M1yHIBLSMVtiLw6Wpr', - _score: 0.0, - _source: { - host: { - id: 'hostId', - }, - }, - }, - ], - }, - } as unknown) as SearchResponse) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + mockAgentService.getAgent = jest.fn().mockReturnValue(({ + active: false, + } as unknown) as Agent); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index a5b578f3f981..266d522e8a41 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -63,7 +63,7 @@ describe('query builder', () => { 'test default query params for all endpoints metadata when no params or body is provided ' + 'with unenrolled host ids excluded', async () => { - const unenrolledHostId = '1fdca33f-799f-49f4-939c-ea4383c77672'; + const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; const mockRequest = httpServerMock.createKibanaRequest({ body: {}, }); @@ -76,7 +76,7 @@ describe('query builder', () => { }, metadataIndexPattern, { - unenrolledHostIds: [unenrolledHostId], + unenrolledAgentIds: [unenrolledElasticAgentId], } ); @@ -86,7 +86,7 @@ describe('query builder', () => { bool: { must_not: { terms: { - 'host.id': ['1fdca33f-799f-49f4-939c-ea4383c77672'], + 'elastic.agent.id': [unenrolledElasticAgentId], }, }, }, @@ -198,7 +198,7 @@ describe('query builder', () => { 'test default query params for all endpoints endpoint metadata excluding unerolled endpoint ' + 'and when body filter is provided', async () => { - const unenrolledHostId = '1fdca33f-799f-49f4-939c-ea4383c77672'; + const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; const mockRequest = httpServerMock.createKibanaRequest({ body: { filter: 'not host.ip:10.140.73.246', @@ -213,7 +213,7 @@ describe('query builder', () => { }, metadataIndexPattern, { - unenrolledHostIds: [unenrolledHostId], + unenrolledAgentIds: [unenrolledElasticAgentId], } ); @@ -226,7 +226,7 @@ describe('query builder', () => { bool: { must_not: { terms: { - 'host.id': [unenrolledHostId], + 'elastic.agent.id': [unenrolledElasticAgentId], }, }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index b6ec91675f24..f6385d271004 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -8,7 +8,7 @@ import { esKuery } from '../../../../../../../src/plugins/data/server'; import { EndpointAppContext } from '../../types'; export interface QueryBuilderOptions { - unenrolledHostIds?: string[]; + unenrolledAgentIds?: string[]; } export async function kibanaRequestToMetadataListESQuery( @@ -22,7 +22,7 @@ export async function kibanaRequestToMetadataListESQuery( const pagingProperties = await getPagingProperties(request, endpointAppContext); return { body: { - query: buildQueryBody(request, queryBuilderOptions?.unenrolledHostIds!), + query: buildQueryBody(request, queryBuilderOptions?.unenrolledAgentIds!), collapse: { field: 'host.id', inner_hits: { @@ -76,21 +76,21 @@ async function getPagingProperties( function buildQueryBody( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, - unerolledHostIds: string[] | undefined + unerolledAgentIds: string[] | undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Record { - const filterUnenrolledHosts = unerolledHostIds && unerolledHostIds.length > 0; + const filterUnenrolledAgents = unerolledAgentIds && unerolledAgentIds.length > 0; if (typeof request?.body?.filter === 'string') { const kqlQuery = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(request.body.filter)); return { bool: { - must: filterUnenrolledHosts + must: filterUnenrolledAgents ? [ { bool: { must_not: { terms: { - 'host.id': unerolledHostIds, + 'elastic.agent.id': unerolledAgentIds, }, }, }, @@ -107,12 +107,12 @@ function buildQueryBody( }, }; } - return filterUnenrolledHosts + return filterUnenrolledAgents ? { bool: { must_not: { terms: { - 'host.id': unerolledHostIds, + 'elastic.agent.id': unerolledAgentIds, }, }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts index 545095a6a0c1..30c8f14287ca 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts @@ -4,144 +4,57 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; -import { - findAllUnenrolledHostIds, - fetchAllUnenrolledHostIdsWithScroll, - HostId, - findUnenrolledHostByHostId, -} from './unenroll'; -import { elasticsearchServiceMock } from '../../../../../../../../src/core/server/mocks'; -import { SearchResponse } from 'elasticsearch'; -import { metadataMirrorIndexPattern } from '../../../../../common/endpoint/constants'; -import { EndpointStatus } from '../../../../../common/endpoint/types'; - -const noUnenrolledEndpoint = () => - Promise.resolve(({ - hits: { - hits: [], - }, - } as unknown) as SearchResponse); - -describe('test find all unenrolled HostId', () => { - let mockScopedClient: jest.Mocked; - - it('can find all hits with scroll', async () => { - const firstHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; - const secondHostId = '2fdca33f-799f-49f4-939c-ea4383c77672'; - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(() => Promise.resolve(createSearchResponse(secondHostId, 'scrollId'))) - .mockImplementationOnce(noUnenrolledEndpoint); - - const initialResponse = createSearchResponse(firstHostId, 'initialScrollId'); - const hostIds = await fetchAllUnenrolledHostIdsWithScroll( - initialResponse, - mockScopedClient.callAsCurrentUser - ); - - expect(hostIds).toEqual([{ host: { id: firstHostId } }, { host: { id: secondHostId } }]); +import { SavedObjectsClientContract } from 'kibana/server'; +import { findAllUnenrolledAgentIds } from './unenroll'; +import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; +import { AgentService } from '../../../../../../ingest_manager/server/services'; +import { createMockAgentService } from '../../../mocks'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; + +describe('test find all unenrolled Agent id', () => { + let mockSavedObjectClient: jest.Mocked; + let mockAgentService: jest.Mocked; + beforeEach(() => { + mockSavedObjectClient = savedObjectsClientMock.create(); + mockAgentService = createMockAgentService(); }); - it('can find all unerolled endpoint host ids', async () => { - const firstEndpointHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; - const secondEndpointHostId = '2fdca33f-799f-49f4-939c-ea4383c77672'; - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClient.callAsCurrentUser + it('can find all unerolled endpoint agent ids', async () => { + mockAgentService.listAgents .mockImplementationOnce(() => - Promise.resolve(createSearchResponse(firstEndpointHostId, 'initialScrollId')) + Promise.resolve({ + agents: [ + ({ + id: 'id1', + } as unknown) as Agent, + ], + total: 2, + page: 1, + perPage: 1, + }) ) .mockImplementationOnce(() => - Promise.resolve(createSearchResponse(secondEndpointHostId, 'scrollId')) - ) - .mockImplementationOnce(noUnenrolledEndpoint); - const hostIds = await findAllUnenrolledHostIds(mockScopedClient); - - expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]).toEqual({ - index: metadataMirrorIndexPattern, - scroll: '30s', - body: { - size: 1000, - _source: ['host.id'], - query: { - bool: { - filter: { - term: { - 'Endpoint.status': EndpointStatus.unenrolled, - }, - }, - }, - }, - }, - }); - expect(hostIds).toEqual([ - { host: { id: firstEndpointHostId } }, - { host: { id: secondEndpointHostId } }, - ]); - }); -}); - -describe('test find unenrolled endpoint host id by hostId', () => { - let mockScopedClient: jest.Mocked; - - it('can find unenrolled endpoint by the host id when unenrolled', async () => { - const firstEndpointHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createSearchResponse(firstEndpointHostId, 'initialScrollId')) - ); - const endpointHostId = await findUnenrolledHostByHostId(mockScopedClient, firstEndpointHostId); - expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.index).toEqual( - metadataMirrorIndexPattern - ); - expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body).toEqual({ - size: 1, - _source: ['host.id'], - query: { - bool: { - filter: [ - { - term: { - 'Endpoint.status': EndpointStatus.unenrolled, - }, - }, - { - term: { - 'host.id': firstEndpointHostId, - }, - }, + Promise.resolve({ + agents: [ + ({ + id: 'id2', + } as unknown) as Agent, ], - }, - }, - }); - expect(endpointHostId).toEqual({ host: { id: firstEndpointHostId } }); - }); - - it('find unenrolled endpoint host by the host id return undefined when no unenrolled host', async () => { - const firstHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(noUnenrolledEndpoint); - const hostId = await findUnenrolledHostByHostId(mockScopedClient, firstHostId); - expect(hostId).toBeFalsy(); + total: 2, + page: 1, + perPage: 1, + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 2, + page: 1, + perPage: 1, + }) + ); + const agentIds = await findAllUnenrolledAgentIds(mockAgentService, mockSavedObjectClient); + expect(agentIds).toBeTruthy(); + expect(agentIds).toEqual(['id1', 'id2']); }); }); - -function createSearchResponse(hostId: string, scrollId: string): SearchResponse { - return ({ - hits: { - hits: [ - { - _index: metadataMirrorIndexPattern, - _id: 'S5M1yHIBLSMVtiLw6Wpr', - _score: 0.0, - _source: { - host: { - id: hostId, - }, - }, - }, - ], - }, - _scroll_id: scrollId, - } as unknown) as SearchResponse; -} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts index 332f969ddf7e..bba9d921310d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts @@ -4,113 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, ILegacyScopedClusterClient } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; -import { metadataMirrorIndexPattern } from '../../../../../common/endpoint/constants'; -import { EndpointStatus } from '../../../../../common/endpoint/types'; - -const KEEPALIVE = '30s'; -const SIZE = 1000; - -export interface HostId { - host: { - id: string; - }; -} - -interface HitSource { - _source: HostId; -} - -export async function findUnenrolledHostByHostId( - client: ILegacyScopedClusterClient, - hostId: string -): Promise { - const queryParams = { - index: metadataMirrorIndexPattern, - body: { - size: 1, - _source: ['host.id'], - query: { - bool: { - filter: [ - { - term: { - 'Endpoint.status': EndpointStatus.unenrolled, - }, - }, - { - term: { - 'host.id': hostId, - }, - }, - ], - }, - }, - }, - }; - - const response = (await client.callAsCurrentUser('search', queryParams)) as SearchResponse< - HostId - >; - const newHits = response.hits?.hits || []; - - if (newHits.length > 0) { - const hostIds = newHits.map((hitSource: HitSource) => hitSource._source); - return hostIds[0]; - } else { - return undefined; - } -} - -export async function findAllUnenrolledHostIds( - client: ILegacyScopedClusterClient -): Promise { - const queryParams = { - index: metadataMirrorIndexPattern, - scroll: KEEPALIVE, - body: { - size: SIZE, - _source: ['host.id'], - query: { - bool: { - filter: { - term: { - 'Endpoint.status': EndpointStatus.unenrolled, - }, - }, - }, - }, - }, +import { SavedObjectsClientContract } from 'kibana/server'; +import { AgentService } from '../../../../../../ingest_manager/server'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; + +export async function findAllUnenrolledAgentIds( + agentService: AgentService, + soClient: SavedObjectsClientContract, + pageSize: number = 1000 +): Promise { + const searchOptions = (pageNum: number) => { + return { + page: pageNum, + perPage: pageSize, + showInactive: true, + kuery: 'fleet-agents.packages:endpoint AND fleet-agents.active:false', + }; }; - const response = (await client.callAsCurrentUser('search', queryParams)) as SearchResponse< - HostId - >; - - return fetchAllUnenrolledHostIdsWithScroll(response, client.callAsCurrentUser); -} - -export async function fetchAllUnenrolledHostIdsWithScroll( - response: SearchResponse, - client: LegacyAPICaller, - hits: HostId[] = [] -): Promise { - let newHits = response.hits?.hits || []; - let scrollId = response._scroll_id; - while (newHits.length > 0) { - const hostIds: HostId[] = newHits.map((hitSource: HitSource) => hitSource._source); - hits.push(...hostIds); + let page = 1; - const innerResponse = await client('scroll', { - body: { - scroll: KEEPALIVE, - scroll_id: scrollId, - }, - }); + const result: string[] = []; + let hasMore = true; - newHits = innerResponse.hits?.hits || []; - scrollId = innerResponse._scroll_id; + while (hasMore) { + const unenrolledAgents = await agentService.listAgents(soClient, searchOptions(page++)); + result.push(...unenrolledAgents.agents.map((agent: Agent) => agent.id)); + hasMore = unenrolledAgents.agents.length > 0; } - return hits; + return result; } diff --git a/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts b/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts index d2e99a80ef8a..b239ab41e41f 100644 --- a/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts +++ b/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts @@ -10,7 +10,6 @@ import { eventsIndexPattern, alertsIndexPattern, policyIndexPattern, - metadataMirrorIndexPattern, } from '../../../../plugins/security_solution/common/endpoint/constants'; export async function deleteDataStream(getService: (serviceName: 'es') => Client, index: string) { @@ -30,10 +29,6 @@ export async function deleteMetadataStream(getService: (serviceName: 'es') => Cl await deleteDataStream(getService, metadataIndexPattern); } -export async function deleteMetadataMirrorStream(getService: (serviceName: 'es') => Client) { - await deleteDataStream(getService, metadataMirrorIndexPattern); -} - export async function deleteEventsStream(getService: (serviceName: 'es') => Client) { await deleteDataStream(getService, eventsIndexPattern); } diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 0d77486e0753..41531269ddeb 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { deleteMetadataMirrorStream, deleteMetadataStream } from './data_stream_helper'; +import { deleteMetadataStream } from './data_stream_helper'; /** * The number of host documents in the es archive. @@ -33,40 +33,6 @@ export default function ({ getService }: FtrProviderContext) { }); }); - describe('POST /api/endpoint/metadata when metadata mirror index contains unenrolled host', () => { - before(async () => { - await esArchiver.load('endpoint/metadata/unenroll_feature/metadata', { useCreate: true }); - await esArchiver.load('endpoint/metadata/unenroll_feature/metadata_mirror', { - useCreate: true, - }); - }); - - after(async () => { - await deleteMetadataStream(getService); - await deleteMetadataMirrorStream(getService); - }); - - it('metadata api should return only enrolled host', async () => { - const { body } = await supertest - .post('/api/endpoint/metadata') - .set('kbn-xsrf', 'xxx') - .send() - .expect(200); - expect(body.total).to.eql(1); - expect(body.hosts.length).to.eql(1); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - }); - - it('metadata api should return 400 when an unenrolled host is retrieved', async () => { - const { body } = await supertest - .get('/api/endpoint/metadata/1fdca33f-799f-49f4-939c-ea4383c77671') - .send() - .expect(400); - expect(body.message).to.eql('the requested endpoint is unenrolled'); - }); - }); - describe('POST /api/endpoint/metadata when index is not empty', () => { before( async () => await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }) diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz b/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz deleted file mode 100644 index d7b130e40515..000000000000 Binary files a/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz and /dev/null differ diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz b/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz deleted file mode 100644 index 3b4da7c47d9f..000000000000 Binary files a/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz and /dev/null differ