[Security Solution] migrate to new GET metadata list API (#119123)

This commit is contained in:
Joey F. Poon 2021-11-23 10:39:14 -06:00 committed by GitHub
parent 079db9666e
commit edf66a52d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 273 additions and 180 deletions

View file

@ -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();
});
});
});

View file

@ -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<typeof GetMetadataListRequestSchemaV2.query>;

View file

@ -1235,18 +1235,14 @@ export interface ListPageRouteState {
/**
* REST API standard base response for list types
*/
export interface BaseListResponse {
data: unknown[];
interface BaseListResponse<D = unknown> {
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<HostInfo>;

View file

@ -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<EndpointMetadataHttpMocksInterface>(
@ -72,6 +72,30 @@ export const endpointMetadataHttpMocks = httpHandlerMockFactory<EndpointMetadata
};
},
},
{
id: 'metadataList',
path: HOST_METADATA_LIST_ROUTE,
method: 'get',
handler: () => {
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,

View file

@ -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 {

View file

@ -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: '',
},
});
});
});

View file

@ -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);
});
});

View file

@ -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<never>;
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<Location> = {}) => {
@ -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,

View file

@ -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<number> => {
try {
return (
await http.post<HostResultList>(HOST_METADATA_LIST_ROUTE, {
body: JSON.stringify({
paging_properties: [{ page_index: 0 }, { page_size: 1 }],
}),
await http.get<MetadataListResponse>(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<HostResultList>(HOST_METADATA_LIST_ROUTE, {
body: JSON.stringify({
paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }],
filters: { kql: decodedQuery.query },
}),
endpointResponse = await coreStart.http.get<MetadataListResponse>(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<HostResultList>(HOST_METADATA_LIST_ROUTE, {
body: JSON.stringify({
paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }],
}),
const response = await coreStart.http.get<MetadataListResponse>(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',

View file

@ -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<typeof endpointListApiPathHandlerMocks>[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;

View file

@ -95,20 +95,13 @@ const handleMetadataTransformStatsChanged: CaseReducer<MetadataTransformStatsCha
/* eslint-disable-next-line complexity */
export const endpointListReducer: StateReducer = (state = initialEndpointPageState(), action) => {
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,
};

View file

@ -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(<EndpointAgentStatus {...props} />);
return renderResult;

View file

@ -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<typeof render>;
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<AppContextTestRender['render']>;
const mockEndpointListApi = () => {
const { hosts } = mockEndpointResultList();
const { data: hosts } = mockEndpointResultList();
hostInfo = {
host_status: hosts[0].host_status,
metadata: {

View file

@ -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(<ManagementContainer />);
mockedContext.history.push('/administration/endpoints');
});

View file

@ -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<typeof GetMetadataListRequestSchemaV2.query>,
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<typeof GetMetadataListRequestSchemaV2.query>
queryOptions: GetMetadataListRequestQuery
): Promise<HostResultList> {
// 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,
});

View file

@ -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

View file

@ -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<typeof GetMetadataListRequestSchemaV2.query>,
queryOptions: GetMetadataListRequestQuery,
endpointPolicyIds: string[] = []
): Promise<BuildUnitedIndexQueryResponse> {
const { page = 0, pageSize = 10, hostStatuses = [], kuery = '' } = queryOptions || {};
const statusesKuery = buildStatusesKuery(hostStatuses);
const filterIgnoredAgents = {

View file

@ -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<string[]> {
if (!statuses.length) {

View file

@ -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<AgentPolicy, 'package_policies'> & {
package_policies: PackagePolicy[];
@ -403,7 +402,7 @@ export class EndpointMetadataService {
*/
async getHostMetadataList(
esClient: ElasticsearchClient,
queryOptions: TypeOf<typeof GetMetadataListRequestSchemaV2.query>
queryOptions: GetMetadataListRequestQuery
): Promise<Pick<MetadataListResponse, 'data' | 'total'>> {
const endpointPolicies = await getAllEndpointPackagePolicies(
this.packagePolicyService,