From edf66a52d28b080c66dce7207b6bfe150b1b3520 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Tue, 23 Nov 2021 10:39:14 -0600 Subject: [PATCH] [Security Solution] migrate to new GET metadata list API (#119123) --- .../common/endpoint/schema/metadata.test.ts | 69 +++++++++++++++++++ .../common/endpoint/schema/metadata.ts | 33 +++++++++ .../common/endpoint/types/index.ts | 10 +-- .../management/pages/endpoint_hosts/mocks.ts | 28 +++++++- .../pages/endpoint_hosts/store/action.ts | 4 +- .../store/endpoint_pagination.test.ts | 29 +++++--- .../pages/endpoint_hosts/store/index.test.ts | 12 ++-- .../endpoint_hosts/store/middleware.test.ts | 44 ++++++------ .../pages/endpoint_hosts/store/middleware.ts | 41 ++++++----- .../store/mock_endpoint_result_list.ts | 68 +++++++----------- .../pages/endpoint_hosts/store/reducer.ts | 13 +--- .../components/endpoint_agent_status.test.tsx | 2 +- .../pages/endpoint_hosts/view/index.test.tsx | 25 ++++--- .../public/management/pages/index.test.tsx | 2 + .../endpoint/routes/metadata/handlers.ts | 35 +++++----- .../server/endpoint/routes/metadata/index.ts | 19 +---- .../routes/metadata/query_builders.ts | 12 ++-- .../routes/metadata/support/agent_status.ts | 2 +- .../metadata/endpoint_metadata_service.ts | 5 +- 19 files changed, 273 insertions(+), 180 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/schema/metadata.test.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/schema/metadata.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/metadata.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/metadata.test.ts new file mode 100644 index 0000000000000..b35546b2bdd66 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/schema/metadata.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { HostStatus } from '../types'; +import { GetMetadataListRequestSchemaV2 } from './metadata'; + +describe('endpoint metadata schema', () => { + describe('GetMetadataListRequestSchemaV2', () => { + const query = GetMetadataListRequestSchemaV2.query; + + it('should return correct query params when valid', () => { + const queryParams = { + page: 1, + pageSize: 20, + kuery: 'some kuery', + hostStatuses: [HostStatus.HEALTHY.toString()], + }; + expect(query.validate(queryParams)).toEqual(queryParams); + }); + + it('should correctly use default values', () => { + const expected = { page: 0, pageSize: 10 }; + expect(query.validate(undefined)).toEqual(expected); + expect(query.validate({ page: undefined })).toEqual(expected); + expect(query.validate({ pageSize: undefined })).toEqual(expected); + expect(query.validate({ page: undefined, pageSize: undefined })).toEqual(expected); + }); + + it('should throw if page param is not a number', () => { + expect(() => query.validate({ page: 'notanumber' })).toThrowError(); + }); + + it('should throw if page param is less than 0', () => { + expect(() => query.validate({ page: -1 })).toThrowError(); + }); + + it('should throw if pageSize param is not a number', () => { + expect(() => query.validate({ pageSize: 'notanumber' })).toThrowError(); + }); + + it('should throw if pageSize param is less than 1', () => { + expect(() => query.validate({ pageSize: 0 })).toThrowError(); + }); + + it('should throw if pageSize param is greater than 10000', () => { + expect(() => query.validate({ pageSize: 10001 })).toThrowError(); + }); + + it('should throw if kuery is not string', () => { + expect(() => query.validate({ kuery: 123 })).toThrowError(); + }); + + it('should work with valid hostStatus', () => { + const queryParams = { hostStatuses: [HostStatus.HEALTHY, HostStatus.UPDATING] }; + const expected = { page: 0, pageSize: 10, ...queryParams }; + expect(query.validate(queryParams)).toEqual(expected); + }); + + it('should throw if invalid hostStatus', () => { + expect(() => + query.validate({ hostStatuses: [HostStatus.UNHEALTHY, 'invalidstatus'] }) + ).toThrowError(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/metadata.ts b/x-pack/plugins/security_solution/common/endpoint/schema/metadata.ts new file mode 100644 index 0000000000000..441b8c3826fb4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/schema/metadata.ts @@ -0,0 +1,33 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { HostStatus } from '../types'; + +export const GetMetadataListRequestSchemaV2 = { + query: schema.object( + { + page: schema.number({ defaultValue: 0, min: 0 }), + pageSize: schema.number({ defaultValue: 10, min: 1, max: 10000 }), + kuery: schema.maybe(schema.string()), + hostStatuses: schema.maybe( + schema.arrayOf( + schema.oneOf([ + schema.literal(HostStatus.HEALTHY.toString()), + schema.literal(HostStatus.OFFLINE.toString()), + schema.literal(HostStatus.UPDATING.toString()), + schema.literal(HostStatus.UNHEALTHY.toString()), + schema.literal(HostStatus.INACTIVE.toString()), + ]) + ) + ), + }, + { defaultValue: { page: 0, pageSize: 10 } } + ), +}; + +export type GetMetadataListRequestQuery = TypeOf; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 2dc4f49919ef7..c869c9c780bd9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -1235,18 +1235,14 @@ export interface ListPageRouteState { /** * REST API standard base response for list types */ -export interface BaseListResponse { - data: unknown[]; +interface BaseListResponse { + data: D[]; page: number; pageSize: number; total: number; - sort?: string; - sortOrder?: 'asc' | 'desc'; } /** * Returned by the server via GET /api/endpoint/metadata */ -export interface MetadataListResponse extends BaseListResponse { - data: HostInfo[]; -} +export type MetadataListResponse = BaseListResponse; 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 c724773593f53..781c332430c0f 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 @@ -14,8 +14,8 @@ import { ActivityLog, HostInfo, HostPolicyResponse, - HostResultList, HostStatus, + MetadataListResponse, } from '../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { FleetActionGenerator } from '../../../../common/endpoint/data_generators/fleet_action_generator'; @@ -43,7 +43,7 @@ import { } from '../mocks'; type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{ - metadataList: () => HostResultList; + metadataList: () => MetadataListResponse; metadataDetails: () => HostInfo; }>; export const endpointMetadataHttpMocks = httpHandlerMockFactory( @@ -72,6 +72,30 @@ export const endpointMetadataHttpMocks = httpHandlerMockFactory { + const generator = new EndpointDocGenerator('seed'); + + return { + data: Array.from({ length: 10 }, () => { + const endpoint = { + metadata: generator.generateHostMetadata(), + host_status: HostStatus.UNHEALTHY, + }; + + generator.updateCommonInfo(); + + return endpoint; + }), + total: 10, + page: 0, + pageSize: 10, + }; + }, + }, { id: 'metadataDetails', path: HOST_METADATA_GET_ROUTE, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index c838f0bee7c69..078507989505f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -9,11 +9,11 @@ import { Action } from 'redux'; import { EuiSuperDatePickerRecentRange } from '@elastic/eui'; import type { DataViewBase } from '@kbn/es-query'; import { - HostResultList, HostInfo, GetHostPolicyResponse, HostIsolationRequestBody, ISOLATION_ACTIONS, + MetadataListResponse, } from '../../../../../common/endpoint/types'; import { ServerApiError } from '../../../../common/types'; import { GetPolicyListResponse } from '../../policy/types'; @@ -21,7 +21,7 @@ import { EndpointState } from '../types'; export interface ServerReturnedEndpointList { type: 'serverReturnedEndpointList'; - payload: HostResultList; + payload: MetadataListResponse; } export interface ServerFailedToReturnEndpointList { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts index dcfd5c86d11d8..214fc220e04fb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts @@ -11,7 +11,7 @@ import { applyMiddleware, Store, createStore } from 'redux'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; -import { HostResultList, AppLocation } from '../../../../../common/endpoint/types'; +import { AppLocation, MetadataListResponse } from '../../../../../common/endpoint/types'; import { DepsStartMock, depsStartMock } from '../../../../common/mock/endpoint'; import { endpointMiddlewareFactory } from './middleware'; @@ -19,13 +19,17 @@ import { endpointMiddlewareFactory } from './middleware'; import { endpointListReducer } from './reducer'; import { uiQueryParams } from './selectors'; -import { mockEndpointResultList } from './mock_endpoint_result_list'; +import { + mockEndpointResultList, + setEndpointListApiMockImplementation, +} from './mock_endpoint_result_list'; import { EndpointState, EndpointIndexUIQueryParams } from '../types'; import { MiddlewareActionSpyHelper, createSpyMiddleware, } from '../../../../common/store/test_utils'; import { getEndpointListPath } from '../../../common/routing'; +import { HOST_METADATA_LIST_ROUTE } from '../../../../../common/endpoint/constants'; jest.mock('../../policy/store/services/ingest', () => ({ sendGetAgentPolicyList: () => Promise.resolve({ items: [] }), @@ -40,8 +44,8 @@ describe('endpoint list pagination: ', () => { let queryParams: () => EndpointIndexUIQueryParams; let waitForAction: MiddlewareActionSpyHelper['waitForAction']; let actionSpyMiddleware; - const getEndpointListApiResponse = (): HostResultList => { - return mockEndpointResultList({ request_page_size: 1, request_page_index: 1, total: 10 }); + const getEndpointListApiResponse = (): MetadataListResponse => { + return mockEndpointResultList({ pageSize: 1, page: 0, total: 10 }); }; let historyPush: (params: EndpointIndexUIQueryParams) => void; @@ -63,13 +67,15 @@ describe('endpoint list pagination: ', () => { historyPush = (nextQueryParams: EndpointIndexUIQueryParams): void => { return history.push(getEndpointListPath({ name: 'endpointList', ...nextQueryParams })); }; + + setEndpointListApiMockImplementation(fakeHttpServices); }); describe('when the user enteres the endpoint list for the first time', () => { it('the api is called with page_index and page_size defaulting to 0 and 10 respectively', async () => { const apiResponse = getEndpointListApiResponse(); - fakeHttpServices.post.mockResolvedValue(apiResponse); - expect(fakeHttpServices.post).not.toHaveBeenCalled(); + fakeHttpServices.get.mockResolvedValue(apiResponse); + expect(fakeHttpServices.get).not.toHaveBeenCalled(); store.dispatch({ type: 'userChangedUrl', @@ -79,11 +85,12 @@ describe('endpoint list pagination: ', () => { }, }); await waitForAction('serverReturnedEndpointList'); - expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { - body: JSON.stringify({ - paging_properties: [{ page_index: '0' }, { page_size: '10' }], - filters: { kql: '' }, - }), + expect(fakeHttpServices.get).toHaveBeenCalledWith(HOST_METADATA_LIST_ROUTE, { + query: { + page: '0', + pageSize: '10', + kuery: '', + }, }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 49ba88fd47717..4edbdef4d2894 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -25,7 +25,7 @@ describe('EndpointList store concerns', () => { const loadDataToStore = () => { dispatch({ type: 'serverReturnedEndpointList', - payload: mockEndpointResultList({ request_page_size: 1, request_page_index: 1, total: 10 }), + payload: mockEndpointResultList({ pageSize: 1, page: 0, total: 10 }), }); }; @@ -101,8 +101,8 @@ describe('EndpointList store concerns', () => { test('it handles `serverReturnedEndpointList', () => { const payload = mockEndpointResultList({ - request_page_size: 1, - request_page_index: 1, + page: 0, + pageSize: 1, total: 10, }); dispatch({ @@ -111,9 +111,9 @@ describe('EndpointList store concerns', () => { }); const currentState = store.getState(); - expect(currentState.hosts).toEqual(payload.hosts); - expect(currentState.pageSize).toEqual(payload.request_page_size); - expect(currentState.pageIndex).toEqual(payload.request_page_index); + expect(currentState.hosts).toEqual(payload.data); + expect(currentState.pageSize).toEqual(payload.pageSize); + expect(currentState.pageIndex).toEqual(payload.page); expect(currentState.total).toEqual(payload.total); }); }); 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 8405320198615..7fc80bffd7c04 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 @@ -16,10 +16,10 @@ import { } from '../../../../common/store/test_utils'; import { Immutable, - HostResultList, HostIsolationResponse, ISOLATION_ACTIONS, ActivityLog, + MetadataListResponse, } from '../../../../../common/endpoint/types'; import { AppAction } from '../../../../common/store/actions'; import { mockEndpointResultList } from './mock_endpoint_result_list'; @@ -72,8 +72,8 @@ describe('endpoint list middleware', () => { let actionSpyMiddleware; let history: History; - const getEndpointListApiResponse = (): HostResultList => { - return mockEndpointResultList({ request_page_size: 1, request_page_index: 1, total: 10 }); + const getEndpointListApiResponse = (): MetadataListResponse => { + return mockEndpointResultList({ pageSize: 1, page: 0, total: 10 }); }; const dispatchUserChangedUrlToEndpointList = (locationOverrides: Partial = {}) => { @@ -105,25 +105,26 @@ describe('endpoint list middleware', () => { it('handles `userChangedUrl`', async () => { endpointPageHttpMock(fakeHttpServices); const apiResponse = getEndpointListApiResponse(); - fakeHttpServices.post.mockResolvedValue(apiResponse); - expect(fakeHttpServices.post).not.toHaveBeenCalled(); + fakeHttpServices.get.mockResolvedValue(apiResponse); + expect(fakeHttpServices.get).not.toHaveBeenCalled(); dispatchUserChangedUrlToEndpointList(); await waitForAction('serverReturnedEndpointList'); - expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { - body: JSON.stringify({ - paging_properties: [{ page_index: '0' }, { page_size: '10' }], - filters: { kql: '' }, - }), + expect(fakeHttpServices.get).toHaveBeenNthCalledWith(1, HOST_METADATA_LIST_ROUTE, { + query: { + page: '0', + pageSize: '10', + kuery: '', + }, }); - expect(listData(getState())).toEqual(apiResponse.hosts); + expect(listData(getState())).toEqual(apiResponse.data); }); it('handles `appRequestedEndpointList`', async () => { endpointPageHttpMock(fakeHttpServices); const apiResponse = getEndpointListApiResponse(); - fakeHttpServices.post.mockResolvedValue(apiResponse); - expect(fakeHttpServices.post).not.toHaveBeenCalled(); + fakeHttpServices.get.mockResolvedValue(apiResponse); + expect(fakeHttpServices.get).not.toHaveBeenCalled(); // First change the URL dispatchUserChangedUrlToEndpointList(); @@ -144,13 +145,14 @@ describe('endpoint list middleware', () => { waitForAction('serverReturnedAgenstWithEndpointsTotal'), ]); - expect(fakeHttpServices.post).toHaveBeenCalledWith(HOST_METADATA_LIST_ROUTE, { - body: JSON.stringify({ - paging_properties: [{ page_index: '0' }, { page_size: '10' }], - filters: { kql: '' }, - }), + expect(fakeHttpServices.get).toHaveBeenNthCalledWith(1, HOST_METADATA_LIST_ROUTE, { + query: { + page: '0', + pageSize: '10', + kuery: '', + }, }); - expect(listData(getState())).toEqual(apiResponse.hosts); + expect(listData(getState())).toEqual(apiResponse.data); }); describe('handling of IsolateEndpointHost action', () => { @@ -242,7 +244,7 @@ describe('endpoint list middleware', () => { }); const endpointList = getEndpointListApiResponse(); - const agentId = endpointList.hosts[0].metadata.agent.id; + const agentId = endpointList.data[0].metadata.agent.id; const search = getEndpointDetailsPath({ name: 'endpointActivityLog', selected_endpoint: agentId, @@ -514,7 +516,7 @@ describe('endpoint list middleware', () => { }); const endpointList = getEndpointListApiResponse(); - const agentId = endpointList.hosts[0].metadata.agent.id; + const agentId = endpointList.data[0].metadata.agent.id; const search = getEndpointDetailsPath({ name: 'endpointDetails', selected_endpoint: agentId, 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 3f4afe8e4b108..d82518c303c6e 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 @@ -19,6 +19,7 @@ import { HostResultList, Immutable, ImmutableObject, + MetadataListResponse, } from '../../../../../common/endpoint/types'; import { GetPolicyListResponse } from '../../policy/types'; import { ImmutableMiddlewareAPI, ImmutableMiddlewareFactory } from '../../../../common/store'; @@ -246,10 +247,11 @@ const getAgentAndPoliciesForEndpointsList = async ( const endpointsTotal = async (http: HttpStart): Promise => { try { return ( - await http.post(HOST_METADATA_LIST_ROUTE, { - body: JSON.stringify({ - paging_properties: [{ page_index: 0 }, { page_size: 1 }], - }), + await http.get(HOST_METADATA_LIST_ROUTE, { + query: { + page: 0, + pageSize: 1, + }, }) ).total; } catch (error) { @@ -401,18 +403,18 @@ async function endpointDetailsListMiddleware({ const { getState, dispatch } = store; const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(getState()); - let endpointResponse; + let endpointResponse: MetadataListResponse | undefined; try { const decodedQuery: Query = searchBarQuery(getState()); - endpointResponse = await coreStart.http.post(HOST_METADATA_LIST_ROUTE, { - body: JSON.stringify({ - paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], - filters: { kql: decodedQuery.query }, - }), + endpointResponse = await coreStart.http.get(HOST_METADATA_LIST_ROUTE, { + query: { + page: pageIndex, + pageSize, + kuery: decodedQuery.query as string, + }, }); - endpointResponse.request_page_index = Number(pageIndex); dispatch({ type: 'serverReturnedEndpointList', @@ -447,7 +449,7 @@ async function endpointDetailsListMiddleware({ }); } - dispatchIngestPolicies({ http: coreStart.http, hosts: endpointResponse.hosts, store }); + dispatchIngestPolicies({ http: coreStart.http, hosts: endpointResponse.data, store }); } catch (error) { dispatch({ type: 'serverFailedToReturnEndpointList', @@ -474,7 +476,7 @@ async function endpointDetailsListMiddleware({ } // No endpoints, so we should check to see if there are policies for onboarding - if (endpointResponse && endpointResponse.hosts.length === 0) { + if (endpointResponse && endpointResponse.data.length === 0) { const http = coreStart.http; // The original query to the list could have had an invalid param (ex. invalid page_size), @@ -611,18 +613,19 @@ async function endpointDetailsMiddleware({ if (listData(getState()).length === 0) { const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(getState()); try { - const response = await coreStart.http.post(HOST_METADATA_LIST_ROUTE, { - body: JSON.stringify({ - paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], - }), + const response = await coreStart.http.get(HOST_METADATA_LIST_ROUTE, { + query: { + page: pageIndex, + pageSize, + }, }); - response.request_page_index = Number(pageIndex); + dispatch({ type: 'serverReturnedEndpointList', payload: response, }); - dispatchIngestPolicies({ http: coreStart.http, hosts: response.hosts, store }); + dispatchIngestPolicies({ http: coreStart.http, hosts: response.data, store }); } catch (error) { dispatch({ type: 'serverFailedToReturnEndpointList', 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 2e3de427e6960..61eb5ad3c541d 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 @@ -10,8 +10,8 @@ import { GetHostPolicyResponse, HostInfo, HostPolicyResponse, - HostResultList, HostStatus, + MetadataListResponse, PendingActionsResponse, } from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; @@ -29,7 +29,10 @@ import { } from '../../../../../../fleet/common/types/rest_spec'; import { GetPolicyListResponse } from '../../policy/types'; import { pendingActionsResponseMock } from '../../../../common/lib/endpoint_pending_actions/mocks'; -import { ACTION_STATUS_ROUTE } from '../../../../../common/endpoint/constants'; +import { + ACTION_STATUS_ROUTE, + HOST_METADATA_LIST_ROUTE, +} from '../../../../../common/endpoint/constants'; import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants'; import { TransformStats, TransformStatsResponse } from '../types'; @@ -37,20 +40,16 @@ const generator = new EndpointDocGenerator('seed'); export const mockEndpointResultList: (options?: { total?: number; - request_page_size?: number; - request_page_index?: number; -}) => HostResultList = (options = {}) => { - const { - total = 1, - request_page_size: requestPageSize = 10, - request_page_index: requestPageIndex = 0, - } = options; + page?: number; + pageSize?: number; +}) => MetadataListResponse = (options = {}) => { + const { total = 1, page = 0, pageSize = 10 } = options; // Skip any that are before the page we're on - const numberToSkip = requestPageSize * requestPageIndex; + const numberToSkip = pageSize * page; // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 - const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); + const actualCountToReturn = Math.max(Math.min(total - numberToSkip, pageSize), 0); const hosts: HostInfo[] = []; for (let index = 0; index < actualCountToReturn; index++) { @@ -59,11 +58,11 @@ export const mockEndpointResultList: (options?: { host_status: HostStatus.UNHEALTHY, }); } - const mock: HostResultList = { - hosts, + const mock: MetadataListResponse = { + data: hosts, total, - request_page_size: requestPageSize, - request_page_index: requestPageIndex, + page, + pageSize, }; return mock; }; @@ -83,7 +82,7 @@ export const mockEndpointDetailsApiResult = (): HostInfo => { * API handlers for Host details based on a list of Host results. */ const endpointListApiPathHandlerMocks = ({ - endpointsResults = mockEndpointResultList({ total: 3 }).hosts, + endpointsResults = mockEndpointResultList({ total: 3 }).data, epmPackages = [generator.generateEpmPackage()], endpointPackagePolicies = [], policyResponse = generator.generatePolicyResponse(), @@ -92,7 +91,7 @@ const endpointListApiPathHandlerMocks = ({ transforms = [], }: { /** route handlers will be setup for each individual host in this array */ - endpointsResults?: HostResultList['hosts']; + endpointsResults?: MetadataListResponse['data']; epmPackages?: GetPackagesResponse['response']; endpointPackagePolicies?: GetPolicyListResponse['items']; policyResponse?: HostPolicyResponse; @@ -109,12 +108,12 @@ const endpointListApiPathHandlerMocks = ({ }, // endpoint list - '/api/endpoint/metadata': (): HostResultList => { + [HOST_METADATA_LIST_ROUTE]: (): MetadataListResponse => { return { - hosts: endpointsResults, - request_page_size: 10, - request_page_index: 0, + data: endpointsResults, total: endpointsResults?.length || 0, + page: 0, + pageSize: 10, }; }, @@ -173,7 +172,7 @@ const endpointListApiPathHandlerMocks = ({ if (endpointsResults) { endpointsResults.forEach((host) => { // @ts-expect-error - apiHandlers[`/api/endpoint/metadata/${host.metadata.agent.id}`] = () => host; + apiHandlers[`${HOST_METADATA_LIST_ROUTE}/${host.metadata.agent.id}`] = () => host; }); } @@ -192,34 +191,13 @@ export const setEndpointListApiMockImplementation: ( apiResponses?: Parameters[0] ) => void = ( mockedHttpService, - { endpointsResults = mockEndpointResultList({ total: 3 }).hosts, ...pathHandlersOptions } = {} + { endpointsResults = mockEndpointResultList({ total: 3 }).data, ...pathHandlersOptions } = {} ) => { const apiHandlers = endpointListApiPathHandlerMocks({ ...pathHandlersOptions, endpointsResults, }); - mockedHttpService.post - .mockImplementation(async (...args) => { - throw new Error(`un-expected call to http.post: ${args}`); - }) - // First time called, return list of endpoints - .mockImplementationOnce(async () => { - return apiHandlers['/api/endpoint/metadata'](); - }) - // Metadata is called a second time to get the full total of Endpoints regardless of filters. - .mockImplementationOnce(async () => { - return apiHandlers['/api/endpoint/metadata'](); - }); - - // If the endpoints list results is zero, then mock the third call to `/metadata` to return - // empty list - indicating there are no endpoints currently present on the system - if (!endpointsResults.length) { - mockedHttpService.post.mockImplementationOnce(async () => { - return apiHandlers['/api/endpoint/metadata'](); - }); - } - // Setup handling of GET requests mockedHttpService.get.mockImplementation(async (...args) => { const [path] = args; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index d9407e310639e..60a93e10e4f7f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -95,20 +95,13 @@ const handleMetadataTransformStatsChanged: CaseReducer { if (action.type === 'serverReturnedEndpointList') { - const { - hosts, - total, - request_page_size: pageSize, - request_page_index: pageIndex, - policy_info: policyVersionInfo, - } = action.payload; + const { data, total, page, pageSize } = action.payload; return { ...state, - hosts, + hosts: data, total, + pageIndex: page, pageSize, - pageIndex, - policyVersionInfo, loading: false, error: undefined, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx index 164b69b3f8bb6..6b2bfc25d5c8a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx @@ -34,7 +34,7 @@ describe('When using the EndpointAgentStatus component', () => { (KibanaServices.get as jest.Mock).mockReturnValue(mockedContext.startServices); httpMocks = endpointPageHttpMock(mockedContext.coreStart.http); waitForAction = mockedContext.middlewareSpy.waitForAction; - endpointMeta = httpMocks.responseProvider.metadataList().hosts[0].metadata; + endpointMeta = httpMocks.responseProvider.metadataList().data[0].metadata; render = async (props: EndpointAgentStatusProps) => { renderResult = mockedContext.render(); return renderResult; 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 a71acd66650dc..6ce9f1df915f8 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 @@ -52,6 +52,7 @@ import { } from '../../../../../common/constants'; import { TransformStats } from '../types'; import { + HOST_METADATA_LIST_ROUTE, metadataTransformPrefix, METADATA_UNITED_TRANSFORM, } from '../../../../../common/endpoint/constants'; @@ -170,6 +171,10 @@ describe('when on the endpoint list page', () => { }); it('should NOT display timeline', async () => { + setEndpointListApiMockImplementation(coreStart.http, { + endpointsResults: [], + }); + const renderResult = render(); const timelineFlyout = renderResult.queryByTestId('flyoutOverlay'); expect(timelineFlyout).toBeNull(); @@ -243,7 +248,7 @@ describe('when on the endpoint list page', () => { total: 4, }); setEndpointListApiMockImplementation(coreStart.http, { - endpointsResults: mockedEndpointListData.hosts, + endpointsResults: mockedEndpointListData.data, totalAgentsUsingEndpoint: 5, }); }); @@ -260,7 +265,7 @@ describe('when on the endpoint list page', () => { total: 5, }); setEndpointListApiMockImplementation(coreStart.http, { - endpointsResults: mockedEndpointListData.hosts, + endpointsResults: mockedEndpointListData.data, totalAgentsUsingEndpoint: 5, }); }); @@ -277,7 +282,7 @@ describe('when on the endpoint list page', () => { total: 6, }); setEndpointListApiMockImplementation(coreStart.http, { - endpointsResults: mockedEndpointListData.hosts, + endpointsResults: mockedEndpointListData.data, totalAgentsUsingEndpoint: 5, }); }); @@ -291,6 +296,10 @@ describe('when on the endpoint list page', () => { describe('when there is no selected host in the url', () => { it('should not show the flyout', () => { + setEndpointListApiMockImplementation(coreStart.http, { + endpointsResults: [], + }); + const renderResult = render(); expect.assertions(1); return renderResult.findByTestId('endpointDetailsFlyout').catch((e) => { @@ -307,7 +316,7 @@ describe('when on the endpoint list page', () => { beforeEach(() => { reactTestingLibrary.act(() => { const mockedEndpointData = mockEndpointResultList({ total: 5 }); - const hostListData = mockedEndpointData.hosts; + const hostListData = mockedEndpointData.data; firstPolicyID = hostListData[0].metadata.Endpoint.policy.applied.id; firstPolicyRev = hostListData[0].metadata.Endpoint.policy.applied.endpoint_policy_version; @@ -518,7 +527,7 @@ describe('when on the endpoint list page', () => { describe.skip('when polling on Endpoint List', () => { beforeEach(() => { reactTestingLibrary.act(() => { - const hostListData = mockEndpointResultList({ total: 4 }).hosts; + const hostListData = mockEndpointResultList({ total: 4 }).data; setEndpointListApiMockImplementation(coreStart.http, { endpointsResults: hostListData, @@ -546,7 +555,7 @@ describe('when on the endpoint list page', () => { expect(total[0].textContent).toEqual('4 Hosts'); setEndpointListApiMockImplementation(coreStart.http, { - endpointsResults: mockEndpointResultList({ total: 1 }).hosts, + endpointsResults: mockEndpointResultList({ total: 1 }).data, }); await reactTestingLibrary.act(async () => { @@ -1090,7 +1099,7 @@ describe('when on the endpoint list page', () => { let renderResult: ReturnType; beforeEach(async () => { coreStart.http.post.mockImplementation(async (requestOptions) => { - if (requestOptions.path === '/api/endpoint/metadata') { + if (requestOptions.path === HOST_METADATA_LIST_ROUTE) { return mockEndpointResultList({ total: 0 }); } throw new Error(`POST to '${requestOptions.path}' does not have a mock response!`); @@ -1377,7 +1386,7 @@ describe('when on the endpoint list page', () => { let renderResult: ReturnType; const mockEndpointListApi = () => { - const { hosts } = mockEndpointResultList(); + const { data: hosts } = mockEndpointResultList(); hostInfo = { host_status: hosts[0].host_status, metadata: { diff --git a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx index 821e14edfda45..5abdb5020110e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx @@ -11,6 +11,7 @@ import { ManagementContainer } from './index'; import '../../common/mock/match_media.ts'; import { AppContextTestRender, createAppRootMockRenderer } from '../../common/mock/endpoint'; import { useUserPrivileges } from '../../common/components/user_privileges'; +import { endpointPageHttpMock } from './endpoint_hosts/mocks'; jest.mock('../../common/components/user_privileges'); @@ -19,6 +20,7 @@ describe('when in the Administration tab', () => { beforeEach(() => { const mockedContext = createAppRootMockRenderer(); + endpointPageHttpMock(mockedContext.coreStart.http); render = () => mockedContext.render(); mockedContext.history.push('/administration/endpoints'); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index cb5e055206585..0969ea8441c0d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -27,11 +27,7 @@ import { getPagingProperties, kibanaRequestToMetadataListESQuery } from './query import { PackagePolicy } from '../../../../../fleet/common/types/models'; import { AgentNotFoundError } from '../../../../../fleet/server'; import { EndpointAppContext, HostListQueryResult } from '../../types'; -import { - GetMetadataListRequestSchema, - GetMetadataListRequestSchemaV2, - GetMetadataRequestSchema, -} from './index'; +import { GetMetadataListRequestSchema, GetMetadataRequestSchema } from './index'; import { findAllUnenrolledAgentIds } from './support/unenroll'; import { getAllEndpointPackagePolicies } from './support/endpoint_package_policies'; import { findAgentIdsByStatus } from './support/agent_status'; @@ -41,6 +37,7 @@ import { queryResponseToHostListResult } from './support/query_strategies'; import { EndpointError, NotFoundError } from '../../errors'; import { EndpointHostUnEnrolledError } from '../../services/metadata'; import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; +import { GetMetadataListRequestQuery } from '../../../../common/endpoint/schema/metadata'; export interface MetadataRequestContext { esClient?: IScopedClusterClient; @@ -163,15 +160,12 @@ export function getMetadataListRequestHandlerV2( logger: Logger ): RequestHandler< unknown, - TypeOf, + GetMetadataListRequestQuery, unknown, SecuritySolutionRequestHandlerContext > { return async (context, request, response) => { const endpointMetadataService = endpointAppContext.service.getEndpointMetadataService(); - if (!endpointMetadataService) { - throw new EndpointError('endpoint metadata service not available'); - } let doesUnitedIndexExist = false; let didUnitedIndexError = false; @@ -191,6 +185,9 @@ export function getMetadataListRequestHandlerV2( didUnitedIndexError = true; } + const { endpointResultListDefaultPageSize, endpointResultListDefaultFirstPageIndex } = + await endpointAppContext.config(); + // If no unified Index present, then perform a search using the legacy approach if (!doesUnitedIndexExist || didUnitedIndexError) { const endpointPolicies = await getAllEndpointPackagePolicies( @@ -208,8 +205,8 @@ export function getMetadataListRequestHandlerV2( body = { data: legacyResponse.hosts, total: legacyResponse.total, - page: request.query.page, - pageSize: request.query.pageSize, + page: request.query.page || endpointResultListDefaultFirstPageIndex, + pageSize: request.query.pageSize || endpointResultListDefaultPageSize, }; return response.ok({ body }); } @@ -224,8 +221,8 @@ export function getMetadataListRequestHandlerV2( body = { data, total, - page: request.query.page, - pageSize: request.query.pageSize, + page: request.query.page || endpointResultListDefaultFirstPageIndex, + pageSize: request.query.pageSize || endpointResultListDefaultPageSize, }; } catch (error) { return errorHandler(logger, response, error); @@ -396,7 +393,7 @@ async function legacyListMetadataQuery( endpointAppContext: EndpointAppContext, logger: Logger, endpointPolicies: PackagePolicy[], - queryOptions: TypeOf + queryOptions: GetMetadataListRequestQuery ): Promise { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const agentService = endpointAppContext.service.getAgentService()!; @@ -422,13 +419,15 @@ async function legacyListMetadataQuery( const statusAgentIds = await findAgentIdsByStatus( agentService, context.core.elasticsearch.client.asCurrentUser, - queryOptions.hostStatuses + queryOptions?.hostStatuses || [] ); + const { endpointResultListDefaultPageSize, endpointResultListDefaultFirstPageIndex } = + await endpointAppContext.config(); const queryParams = await kibanaRequestToMetadataListESQuery({ - page: queryOptions.page, - pageSize: queryOptions.pageSize, - kuery: queryOptions.kuery, + page: queryOptions?.page || endpointResultListDefaultFirstPageIndex, + pageSize: queryOptions?.pageSize || endpointResultListDefaultPageSize, + kuery: queryOptions?.kuery || '', unenrolledAgentIds, statusAgentIds, }); 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 5ea465aa21799..c0c37c879e801 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 @@ -20,6 +20,7 @@ import { HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, } from '../../../../common/endpoint/constants'; +import { GetMetadataListRequestSchemaV2 } from '../../../../common/endpoint/schema/metadata'; /* Filters that can be applied to the endpoint fetch route */ export const endpointFilters = schema.object({ @@ -65,24 +66,6 @@ export const GetMetadataListRequestSchema = { ), }; -export const GetMetadataListRequestSchemaV2 = { - query: schema.object({ - page: schema.number({ defaultValue: 0 }), - pageSize: schema.number({ defaultValue: 10, min: 1, max: 10000 }), - kuery: schema.maybe(schema.string()), - hostStatuses: schema.arrayOf( - schema.oneOf([ - schema.literal(HostStatus.HEALTHY.toString()), - schema.literal(HostStatus.OFFLINE.toString()), - schema.literal(HostStatus.UPDATING.toString()), - schema.literal(HostStatus.UNHEALTHY.toString()), - schema.literal(HostStatus.INACTIVE.toString()), - ]), - { defaultValue: [] } - ), - }), -}; - export function registerEndpointRoutes( router: SecuritySolutionPluginRouter, endpointAppContext: EndpointAppContext 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 2262028ec43bf..09b8367e8c021 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 @@ -6,7 +6,6 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { TypeOf } from '@kbn/config-schema'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { metadataCurrentIndexPattern, @@ -15,7 +14,7 @@ import { import { KibanaRequest } from '../../../../../../../src/core/server'; import { EndpointAppContext } from '../../types'; import { buildStatusesKuery } from './support/agent_status'; -import { GetMetadataListRequestSchemaV2 } from '.'; +import { GetMetadataListRequestQuery } from '../../../../common/endpoint/schema/metadata'; /** * 00000000-0000-0000-0000-000000000000 is initial Elastic Agent id sent by Endpoint before policy is configured @@ -234,14 +233,11 @@ interface BuildUnitedIndexQueryResponse { } export async function buildUnitedIndexQuery( - { - page = 0, - pageSize = 10, - hostStatuses = [], - kuery = '', - }: TypeOf, + queryOptions: GetMetadataListRequestQuery, endpointPolicyIds: string[] = [] ): Promise { + const { page = 0, pageSize = 10, hostStatuses = [], kuery = '' } = queryOptions || {}; + const statusesKuery = buildStatusesKuery(hostStatuses); const filterIgnoredAgents = { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts index a7781cb77e8c0..f9e04f4edebee 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts @@ -36,7 +36,7 @@ export function buildStatusesKuery(statusesToFilter: string[]): string | undefin export async function findAgentIdsByStatus( agentService: AgentService, esClient: ElasticsearchClient, - statuses: string[] = [], + statuses: string[], pageSize: number = 1000 ): Promise { if (!statuses.length) { 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 832b8b507e5d4..965686ba19000 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 @@ -12,7 +12,6 @@ import { SavedObjectsServiceStart, } from 'kibana/server'; -import { TypeOf } from '@kbn/config-schema'; import { TransportResult } from '@elastic/elasticsearch'; import { SearchTotalHits, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import { @@ -57,7 +56,7 @@ import { createInternalReadonlySoClient } from '../../utils/create_internal_read import { METADATA_UNITED_INDEX } from '../../../../common/endpoint/constants'; import { getAllEndpointPackagePolicies } from '../../routes/metadata/support/endpoint_package_policies'; import { getAgentStatus } from '../../../../../fleet/common/services/agent_status'; -import { GetMetadataListRequestSchemaV2 } from '../../routes/metadata'; +import { GetMetadataListRequestQuery } from '../../../../common/endpoint/schema/metadata'; type AgentPolicyWithPackagePolicies = Omit & { package_policies: PackagePolicy[]; @@ -403,7 +402,7 @@ export class EndpointMetadataService { */ async getHostMetadataList( esClient: ElasticsearchClient, - queryOptions: TypeOf + queryOptions: GetMetadataListRequestQuery ): Promise> { const endpointPolicies = await getAllEndpointPackagePolicies( this.packagePolicyService,