-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
EMT-issue-65: add endpoint list api (#53861)
add endpoint list api
- Loading branch information
1 parent
f46e8e2
commit 6a2fb61
Showing
15 changed files
with
1,009 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
import { EndpointConfigSchema, EndpointConfigType } from './config'; | ||
|
||
describe('test config schema', () => { | ||
it('test config defaults', () => { | ||
const config: EndpointConfigType = EndpointConfigSchema.validate({}); | ||
expect(config.enabled).toEqual(false); | ||
expect(config.endpointResultListDefaultPageSize).toEqual(10); | ||
expect(config.endpointResultListDefaultFirstPageIndex).toEqual(0); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
import { schema, TypeOf } from '@kbn/config-schema'; | ||
import { Observable } from 'rxjs'; | ||
import { PluginInitializerContext } from 'kibana/server'; | ||
|
||
export type EndpointConfigType = ReturnType<typeof createConfig$> extends Observable<infer P> | ||
? P | ||
: ReturnType<typeof createConfig$>; | ||
|
||
export const EndpointConfigSchema = schema.object({ | ||
enabled: schema.boolean({ defaultValue: false }), | ||
endpointResultListDefaultFirstPageIndex: schema.number({ defaultValue: 0 }), | ||
endpointResultListDefaultPageSize: schema.number({ defaultValue: 10 }), | ||
}); | ||
|
||
export function createConfig$(context: PluginInitializerContext) { | ||
return context.config.create<TypeOf<typeof EndpointConfigSchema>>(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
import { CoreSetup } from 'kibana/server'; | ||
import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin'; | ||
import { coreMock } from '../../../../src/core/server/mocks'; | ||
import { PluginSetupContract } from '../../features/server'; | ||
|
||
describe('test endpoint plugin', () => { | ||
let plugin: EndpointPlugin; | ||
let mockCoreSetup: MockedKeys<CoreSetup>; | ||
let mockedEndpointPluginSetupDependencies: jest.Mocked<EndpointPluginSetupDependencies>; | ||
let mockedPluginSetupContract: jest.Mocked<PluginSetupContract>; | ||
beforeEach(() => { | ||
plugin = new EndpointPlugin( | ||
coreMock.createPluginInitializerContext({ | ||
cookieName: 'sid', | ||
sessionTimeout: 1500, | ||
}) | ||
); | ||
|
||
mockCoreSetup = coreMock.createSetup(); | ||
mockedPluginSetupContract = { | ||
registerFeature: jest.fn(), | ||
getFeatures: jest.fn(), | ||
getFeaturesUICapabilities: jest.fn(), | ||
registerLegacyAPI: jest.fn(), | ||
}; | ||
mockedEndpointPluginSetupDependencies = { features: mockedPluginSetupContract }; | ||
}); | ||
|
||
it('test properly setup plugin', async () => { | ||
await plugin.setup(mockCoreSetup, mockedEndpointPluginSetupDependencies); | ||
expect(mockedPluginSetupContract.registerFeature).toBeCalledTimes(1); | ||
expect(mockCoreSetup.http.createRouter).toBeCalledTimes(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
x-pack/plugins/endpoint/server/routes/endpoints.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
import { | ||
IClusterClient, | ||
IRouter, | ||
IScopedClusterClient, | ||
KibanaResponseFactory, | ||
RequestHandler, | ||
RequestHandlerContext, | ||
RouteConfig, | ||
} from 'kibana/server'; | ||
import { | ||
elasticsearchServiceMock, | ||
httpServerMock, | ||
httpServiceMock, | ||
loggingServiceMock, | ||
} from '../../../../../src/core/server/mocks'; | ||
import { EndpointData } from '../types'; | ||
import { SearchResponse } from 'elasticsearch'; | ||
import { EndpointResultList, registerEndpointRoutes } from './endpoints'; | ||
import { EndpointConfigSchema } from '../config'; | ||
import * as data from '../test_data/all_endpoints_data.json'; | ||
|
||
describe('test endpoint route', () => { | ||
let routerMock: jest.Mocked<IRouter>; | ||
let mockResponse: jest.Mocked<KibanaResponseFactory>; | ||
let mockClusterClient: jest.Mocked<IClusterClient>; | ||
let mockScopedClient: jest.Mocked<IScopedClusterClient>; | ||
let routeHandler: RequestHandler<any, any, any>; | ||
let routeConfig: RouteConfig<any, any, any, any>; | ||
|
||
beforeEach(() => { | ||
mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked< | ||
IClusterClient | ||
>; | ||
mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); | ||
mockClusterClient.asScoped.mockReturnValue(mockScopedClient); | ||
routerMock = httpServiceMock.createRouter(); | ||
mockResponse = httpServerMock.createResponseFactory(); | ||
registerEndpointRoutes(routerMock, { | ||
logFactory: loggingServiceMock.create(), | ||
config: () => Promise.resolve(EndpointConfigSchema.validate({})), | ||
}); | ||
}); | ||
|
||
it('test find the latest of all endpoints', async () => { | ||
const mockRequest = httpServerMock.createKibanaRequest({}); | ||
|
||
const response: SearchResponse<EndpointData> = (data as unknown) as SearchResponse< | ||
EndpointData | ||
>; | ||
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); | ||
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => | ||
path.startsWith('/api/endpoint/endpoints') | ||
)!; | ||
|
||
await routeHandler( | ||
({ | ||
core: { | ||
elasticsearch: { | ||
dataClient: mockScopedClient, | ||
}, | ||
}, | ||
} as unknown) as RequestHandlerContext, | ||
mockRequest, | ||
mockResponse | ||
); | ||
|
||
expect(mockScopedClient.callAsCurrentUser).toBeCalled(); | ||
expect(routeConfig.options).toEqual({ authRequired: true }); | ||
expect(mockResponse.ok).toBeCalled(); | ||
const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as EndpointResultList; | ||
expect(endpointResultList.endpoints.length).toEqual(3); | ||
expect(endpointResultList.total).toEqual(3); | ||
expect(endpointResultList.request_index).toEqual(0); | ||
expect(endpointResultList.request_page_size).toEqual(10); | ||
}); | ||
|
||
it('test find the latest of all endpoints with params', async () => { | ||
const mockRequest = httpServerMock.createKibanaRequest({ | ||
body: { | ||
paging_properties: [ | ||
{ | ||
page_size: 10, | ||
}, | ||
{ | ||
page_index: 1, | ||
}, | ||
], | ||
}, | ||
}); | ||
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => | ||
Promise.resolve((data as unknown) as SearchResponse<EndpointData>) | ||
); | ||
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => | ||
path.startsWith('/api/endpoint/endpoints') | ||
)!; | ||
|
||
await routeHandler( | ||
({ | ||
core: { | ||
elasticsearch: { | ||
dataClient: mockScopedClient, | ||
}, | ||
}, | ||
} as unknown) as RequestHandlerContext, | ||
mockRequest, | ||
mockResponse | ||
); | ||
|
||
expect(mockScopedClient.callAsCurrentUser).toBeCalled(); | ||
expect(routeConfig.options).toEqual({ authRequired: true }); | ||
expect(mockResponse.ok).toBeCalled(); | ||
const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as EndpointResultList; | ||
expect(endpointResultList.endpoints.length).toEqual(3); | ||
expect(endpointResultList.total).toEqual(3); | ||
expect(endpointResultList.request_index).toEqual(10); | ||
expect(endpointResultList.request_page_size).toEqual(10); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { IRouter } from 'kibana/server'; | ||
import { SearchResponse } from 'elasticsearch'; | ||
import { schema } from '@kbn/config-schema'; | ||
import { EndpointAppContext, EndpointData } from '../types'; | ||
import { kibanaRequestToEndpointListQuery } from '../services/endpoint/endpoint_query_builders'; | ||
|
||
interface HitSource { | ||
_source: EndpointData; | ||
} | ||
|
||
export interface EndpointResultList { | ||
// the endpoint restricted by the page size | ||
endpoints: EndpointData[]; | ||
// the total number of unique endpoints in the index | ||
total: number; | ||
// the page size requested | ||
request_page_size: number; | ||
// the index requested | ||
request_index: number; | ||
} | ||
|
||
export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { | ||
router.post( | ||
{ | ||
path: '/api/endpoint/endpoints', | ||
validate: { | ||
body: schema.nullable( | ||
schema.object({ | ||
paging_properties: schema.arrayOf( | ||
schema.oneOf([ | ||
// the number of results to return for this request per page | ||
schema.object({ | ||
page_size: schema.number({ defaultValue: 10, min: 1, max: 10000 }), | ||
}), | ||
// the index of the page to return | ||
schema.object({ page_index: schema.number({ defaultValue: 0, min: 0 }) }), | ||
]) | ||
), | ||
}) | ||
), | ||
}, | ||
options: { authRequired: true }, | ||
}, | ||
async (context, req, res) => { | ||
try { | ||
const queryParams = await kibanaRequestToEndpointListQuery(req, endpointAppContext); | ||
const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( | ||
'search', | ||
queryParams | ||
)) as SearchResponse<EndpointData>; | ||
return res.ok({ body: mapToEndpointResultList(queryParams, response) }); | ||
} catch (err) { | ||
return res.internalError({ body: err }); | ||
} | ||
} | ||
); | ||
} | ||
|
||
function mapToEndpointResultList( | ||
queryParams: Record<string, any>, | ||
searchResponse: SearchResponse<EndpointData> | ||
): EndpointResultList { | ||
if (searchResponse.hits.hits.length > 0) { | ||
return { | ||
request_page_size: queryParams.size, | ||
request_index: queryParams.from, | ||
endpoints: searchResponse.hits.hits | ||
.map(response => response.inner_hits.most_recent.hits.hits) | ||
.flatMap(data => data as HitSource) | ||
.map(entry => entry._source), | ||
total: searchResponse.aggregations.total.value, | ||
}; | ||
} else { | ||
return { | ||
request_page_size: queryParams.size, | ||
request_index: queryParams.from, | ||
total: 0, | ||
endpoints: [], | ||
}; | ||
} | ||
} |
Oops, something went wrong.