From 08343b22c2532b5b3243eacf8ffd05d2fc5a0f86 Mon Sep 17 00:00:00 2001 From: nnamdifrankie Date: Mon, 30 Dec 2019 21:06:29 -0500 Subject: [PATCH 1/9] add endpoint list api --- x-pack/plugins/endpoint/server/config.test.ts | 15 + x-pack/plugins/endpoint/server/config.ts | 22 ++ x-pack/plugins/endpoint/server/index.ts | 8 +- x-pack/plugins/endpoint/server/plugin.test.ts | 39 ++ x-pack/plugins/endpoint/server/plugin.ts | 27 +- .../endpoint/server/routes/endpoints.test.ts | 183 +++++++++ .../endpoint/server/routes/endpoints.ts | 76 ++++ .../endpoint/endpoint_query_builders.test.ts | 55 +++ .../endpoint/endpoint_query_builders.ts | 65 ++++ .../server/test_data/all_endpoints_data.json | 348 ++++++++++++++++++ x-pack/plugins/endpoint/server/types.ts | 50 +++ 11 files changed, 881 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/endpoint/server/config.test.ts create mode 100644 x-pack/plugins/endpoint/server/config.ts create mode 100644 x-pack/plugins/endpoint/server/plugin.test.ts create mode 100644 x-pack/plugins/endpoint/server/routes/endpoints.test.ts create mode 100644 x-pack/plugins/endpoint/server/routes/endpoints.ts create mode 100644 x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts create mode 100644 x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts create mode 100644 x-pack/plugins/endpoint/server/test_data/all_endpoints_data.json create mode 100644 x-pack/plugins/endpoint/server/types.ts diff --git a/x-pack/plugins/endpoint/server/config.test.ts b/x-pack/plugins/endpoint/server/config.test.ts new file mode 100644 index 00000000000000..476d84defa1b9c --- /dev/null +++ b/x-pack/plugins/endpoint/server/config.test.ts @@ -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.searchResultDefaultPageSize).toEqual(10); + expect(config.searchResultDefaultFirstPageIndex).toEqual(0); + }); +}); diff --git a/x-pack/plugins/endpoint/server/config.ts b/x-pack/plugins/endpoint/server/config.ts new file mode 100644 index 00000000000000..87265b1c61f6c0 --- /dev/null +++ b/x-pack/plugins/endpoint/server/config.ts @@ -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 extends Observable + ? P + : ReturnType; + +export const EndpointConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), + searchResultDefaultFirstPageIndex: schema.number({ defaultValue: 0 }), + searchResultDefaultPageSize: schema.number({ defaultValue: 10 }), +}); + +export function createConfig$(context: PluginInitializerContext) { + return context.config.create>(); +} diff --git a/x-pack/plugins/endpoint/server/index.ts b/x-pack/plugins/endpoint/server/index.ts index eec836141ea5e2..ae603b7e44449f 100644 --- a/x-pack/plugins/endpoint/server/index.ts +++ b/x-pack/plugins/endpoint/server/index.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; -import { PluginInitializer } from 'src/core/server'; +import { PluginInitializer, PluginInitializerContext } from 'src/core/server'; import { EndpointPlugin, EndpointPluginStart, @@ -13,9 +12,10 @@ import { EndpointPluginStartDependencies, EndpointPluginSetupDependencies, } from './plugin'; +import { EndpointConfigSchema } from './config'; export const config = { - schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), + schema: EndpointConfigSchema, }; export const plugin: PluginInitializer< @@ -23,4 +23,4 @@ export const plugin: PluginInitializer< EndpointPluginStart, EndpointPluginSetupDependencies, EndpointPluginStartDependencies -> = () => new EndpointPlugin(); +> = (initializerContext: PluginInitializerContext) => new EndpointPlugin(initializerContext); diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts new file mode 100644 index 00000000000000..87d373d3a4f34c --- /dev/null +++ b/x-pack/plugins/endpoint/server/plugin.test.ts @@ -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; + let mockedEndpointPluginSetupDependencies: jest.Mocked; + let mockedPluginSetupContract: jest.Mocked; + 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); + }); +}); diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index b41dfee1f78fd6..7ed116ba211407 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -3,9 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup } from 'kibana/server'; +import { Plugin, CoreSetup, PluginInitializerContext, Logger } from 'kibana/server'; +import { first } from 'rxjs/operators'; import { addRoutes } from './routes'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; +import { createConfig$, EndpointConfigType } from './config'; +import { EndpointAppContext } from './types'; +import { registerEndpointRoutes } from './routes/endpoints'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; @@ -23,6 +27,10 @@ export class EndpointPlugin EndpointPluginSetupDependencies, EndpointPluginStartDependencies > { + private readonly logger: Logger; + constructor(private readonly initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get('endpoint'); + } public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) { plugins.features.registerFeature({ id: 'endpoint', @@ -49,10 +57,23 @@ export class EndpointPlugin }, }, }); + const endpointContext = { + logFactory: this.initializerContext.logger, + config: (): Promise => { + return createConfig$(this.initializerContext) + .pipe(first()) + .toPromise(); + }, + } as EndpointAppContext; const router = core.http.createRouter(); addRoutes(router); + registerEndpointRoutes(router, endpointContext); } - public start() {} - public stop() {} + public start() { + this.logger.debug('Starting plugin'); + } + public stop() { + this.logger.debug('Stopping plugin'); + } } diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts new file mode 100644 index 00000000000000..4ba020bdfb0738 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts @@ -0,0 +1,183 @@ +/* + * 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; + let mockResponse: jest.Mocked; + let mockClusterClient: jest.Mocked; + let mockScopedClient: jest.Mocked; + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + 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({ + body: {}, + params: {}, + }); + + const response: SearchResponse = (data as unknown) as SearchResponse< + EndpointData + >; + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/endpoints') + )!; + + await routeHandler( + ({ + core: { + elasticsearch: { + dataClient: mockScopedClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalledWith('search', { + from: 0, + size: 10, + body: { + query: { + match_all: {}, + }, + collapse: { + field: 'machine_id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ created_at: 'desc' }], + }, + }, + aggs: { + total: { + cardinality: { + field: 'machine_id', + }, + }, + }, + sort: [ + { + created_at: { + order: 'desc', + }, + }, + ], + }, + index: 'endpoint-agent*', + }); + 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.requestIndex).toEqual(0); + expect(endpointResultList.requestPageSize).toEqual(10); + }); + + it('test find the latest of all endpoints with params', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + body: {}, + query: { + pageSize: 10, + pageIndex: 1, + }, + }); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve((data as unknown) as SearchResponse) + ); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/endpoints') + )!; + + await routeHandler( + ({ + core: { + elasticsearch: { + dataClient: mockScopedClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalledWith('search', { + from: 10, + size: 10, + body: { + query: { + match_all: {}, + }, + collapse: { + field: 'machine_id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ created_at: 'desc' }], + }, + }, + aggs: { + total: { + cardinality: { + field: 'machine_id', + }, + }, + }, + sort: [ + { + created_at: { + order: 'desc', + }, + }, + ], + }, + index: 'endpoint-agent*', + }); + 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.requestIndex).toEqual(10); + expect(endpointResultList.requestPageSize).toEqual(10); + }); +}); diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.ts b/x-pack/plugins/endpoint/server/routes/endpoints.ts new file mode 100644 index 00000000000000..8cfb4e34e104d3 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/endpoints.ts @@ -0,0 +1,76 @@ +/* + * 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 { AllEndpointsQueryBuilder } from '../services/endpoint/endpoint_query_builders'; + +interface HitSource { + _source: EndpointData; +} + +export interface EndpointResultList { + endpoints: EndpointData[]; + total: number; + requestPageSize: number; + requestIndex: number; +} + +export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { + router.get( + { + path: '/api/endpoint/endpoints', + validate: { + query: schema.object({ + pageSize: schema.number({ defaultValue: 10 }), + pageIndex: schema.number({ defaultValue: 0 }), + }), + }, + options: { authRequired: true }, + }, + async (context, req, res) => { + try { + const queryParams = await new AllEndpointsQueryBuilder( + req, + endpointAppContext + ).toQueryParams(); + const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( + 'search', + queryParams + )) as SearchResponse; + return res.ok({ body: await mapToEndpointResultList(queryParams, response) }); + } catch (err) { + return res.internalError({ body: err }); + } + } + ); +} + +function mapToEndpointResultList( + queryParams: Record, + searchResponse: SearchResponse +): EndpointResultList { + if (searchResponse.hits.hits.length > 0) { + return { + requestPageSize: queryParams.size as number, + requestIndex: queryParams.from as number, + 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 { + requestPageSize: queryParams.size as number, + requestIndex: queryParams.from as number, + total: 0, + endpoints: [], + }; + } +} diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts new file mode 100644 index 00000000000000..cd0b74db329604 --- /dev/null +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts @@ -0,0 +1,55 @@ +/* + * 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 { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; +import { EndpointConfigSchema } from '../../config'; +import { AllEndpointsQueryBuilder } from './endpoint_query_builders'; + +describe('test query builder', () => { + describe('test query builder request processing', () => { + it('test query with no filter defaults', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + body: {}, + }); + const searchQueryBuilder = new AllEndpointsQueryBuilder(mockRequest, { + logFactory: loggingServiceMock.create(), + config: () => Promise.resolve(EndpointConfigSchema.validate({})), + }); + const query = await searchQueryBuilder.toQueryParams(); + expect(query).toEqual({ + body: { + query: { + match_all: {}, + }, + collapse: { + field: 'machine_id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ created_at: 'desc' }], + }, + }, + aggs: { + total: { + cardinality: { + field: 'machine_id', + }, + }, + }, + sort: [ + { + created_at: { + order: 'desc', + }, + }, + ], + }, + from: 0, + size: 10, + index: 'endpoint-agent*', + } as Record); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts new file mode 100644 index 00000000000000..7c4baaa3d145bd --- /dev/null +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts @@ -0,0 +1,65 @@ +/* + * 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 { KibanaRequest } from 'kibana/server'; +import { EndpointAppContext } from '../../types'; + +export class AllEndpointsQueryBuilder { + private readonly request: KibanaRequest; + private readonly endpointAppContext: EndpointAppContext; + + constructor(request: KibanaRequest, endpointAppContext: EndpointAppContext) { + this.request = request; + this.endpointAppContext = endpointAppContext; + } + + async toQueryParams(): Promise> { + const paging = await this.paging(); + return { + body: { + query: this.queryBody(), + collapse: { + field: 'machine_id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ created_at: 'desc' }], + }, + }, + aggs: { + total: { + cardinality: { + field: 'machine_id', + }, + }, + }, + sort: [ + { + created_at: { + order: 'desc', + }, + }, + ], + }, + from: paging.pageIndex * paging.pageSize, + size: paging.pageSize, + index: 'endpoint-agent*', + }; + } + + private queryBody(): Record { + return { + match_all: {}, + }; + } + + async paging() { + const config = await this.endpointAppContext.config(); + return { + pageSize: this.request.query.pageSize || config.searchResultDefaultPageSize, + pageIndex: this.request.query.pageIndex || config.searchResultDefaultFirstPageIndex, + }; + } +} diff --git a/x-pack/plugins/endpoint/server/test_data/all_endpoints_data.json b/x-pack/plugins/endpoint/server/test_data/all_endpoints_data.json new file mode 100644 index 00000000000000..d505b2c929828c --- /dev/null +++ b/x-pack/plugins/endpoint/server/test_data/all_endpoints_data.json @@ -0,0 +1,348 @@ +{ + "took": 3, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 9, + "relation": "eq" + }, + "max_score": null, + "hits": [ + { + "_index": "endpoint-agent", + "_id": "UV_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "606267a9-2e51-42b4-956e-6cc7812e3447", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "natalee-2", + "hostname": "natalee-2.example.com", + "ip": "10.5.220.127", + "mac_address": "17-5f-c9-f8-ca-d6", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=natalee-2,DC=example,DC=com", + "active_directory_hostname": "natalee-2.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "With Eventing", + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "fields": { + "machine_id": [ + "606267a9-2e51-42b4-956e-6cc7812e3447" + ] + }, + "sort": [ + 1577477368377 + ], + "inner_hits": { + "most_recent": { + "hits": { + "total": { + "value": 3, + "relation": "eq" + }, + "max_score": null, + "hits": [ + { + "_index": "endpoint-agent", + "_id": "UV_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "606267a9-2e51-42b4-956e-6cc7812e3447", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "natalee-2", + "hostname": "natalee-2.example.com", + "ip": "10.5.220.127", + "mac_address": "17-5f-c9-f8-ca-d6", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=natalee-2,DC=example,DC=com", + "active_directory_hostname": "natalee-2.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "With Eventing", + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "sort": [ + 1577477368377 + ] + } + ] + } + } + } + }, + { + "_index": "endpoint-agent", + "_id": "Ul_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "8ec625e1-a80c-4c9f-bdfd-496060aa6310", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "luttrell-2", + "hostname": "luttrell-2.example.com", + "ip": "10.246.84.193", + "mac_address": "dc-d-88-14-c3-c6", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=luttrell-2,DC=example,DC=com", + "active_directory_hostname": "luttrell-2.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "Default", + "id": "00000000-0000-0000-0000-000000000000" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "fields": { + "machine_id": [ + "8ec625e1-a80c-4c9f-bdfd-496060aa6310" + ] + }, + "sort": [ + 1577477368377 + ], + "inner_hits": { + "most_recent": { + "hits": { + "total": { + "value": 3, + "relation": "eq" + }, + "max_score": null, + "hits": [ + { + "_index": "endpoint-agent", + "_id": "Ul_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "8ec625e1-a80c-4c9f-bdfd-496060aa6310", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "luttrell-2", + "hostname": "luttrell-2.example.com", + "ip": "10.246.84.193", + "mac_address": "dc-d-88-14-c3-c6", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=luttrell-2,DC=example,DC=com", + "active_directory_hostname": "luttrell-2.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "Default", + "id": "00000000-0000-0000-0000-000000000000" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "sort": [ + 1577477368377 + ] + } + ] + } + } + } + }, + { + "_index": "endpoint-agent", + "_id": "U1_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "853a308c-6e6d-4b92-a32b-2f623b6c8cf4", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "akeylah-7", + "hostname": "akeylah-7.example.com", + "ip": "10.252.242.44", + "mac_address": "27-b9-51-21-31-a", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=akeylah-7,DC=example,DC=com", + "active_directory_hostname": "akeylah-7.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "With Eventing", + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "fields": { + "machine_id": [ + "853a308c-6e6d-4b92-a32b-2f623b6c8cf4" + ] + }, + "sort": [ + 1577477368377 + ], + "inner_hits": { + "most_recent": { + "hits": { + "total": { + "value": 3, + "relation": "eq" + }, + "max_score": null, + "hits": [ + { + "_index": "endpoint-agent", + "_id": "U1_6SG8B9c_DH2QsbOZd", + "_score": null, + "_source": { + "machine_id": "853a308c-6e6d-4b92-a32b-2f623b6c8cf4", + "created_at": "2019-12-27T20:09:28.377Z", + "host": { + "name": "akeylah-7", + "hostname": "akeylah-7.example.com", + "ip": "10.252.242.44", + "mac_address": "27-b9-51-21-31-a", + "os": { + "name": "windows 6.3", + "full": "Windows Server 2012R2" + } + }, + "endpoint": { + "domain": "example.com", + "is_base_image": false, + "active_directory_distinguished_name": "CN=akeylah-7,DC=example,DC=com", + "active_directory_hostname": "akeylah-7.example.com", + "upgrade": { + "status": null, + "updated_at": null + }, + "isolation": { + "status": false, + "request_status": null, + "updated_at": null + }, + "policy": { + "name": "With Eventing", + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" + }, + "sensor": { + "persistence": true, + "status": {} + } + } + }, + "sort": [ + 1577477368377 + ] + } + ] + } + } + } + } + ] + }, + "aggregations": { + "total": { + "value": 3 + } + } +} diff --git a/x-pack/plugins/endpoint/server/types.ts b/x-pack/plugins/endpoint/server/types.ts new file mode 100644 index 00000000000000..5d697d15460f09 --- /dev/null +++ b/x-pack/plugins/endpoint/server/types.ts @@ -0,0 +1,50 @@ +/* + * 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 { LoggerFactory } from 'kibana/server'; +import { EndpointConfigType } from './config'; + +export interface EndpointAppContext { + logFactory: LoggerFactory; + config(): Promise; +} + +export interface EndpointData { + machine_id: string; + created_at: Date; + host: { + name: string; + hostname: string; + ip: string; + mac_address: string; + os: { + name: string; + full: string; + }; + }; + endpoint: { + domain: string; + is_base_image: boolean; + active_directory_distinguished_name: string; + active_directory_hostname: string; + upgrade: { + status?: string; + updated_at?: Date; + }; + isolation: { + status: false; + request_status?: string | boolean; + updated_at?: Date; + }; + policy: { + name: string; + id: string; + }; + sensor: { + persistence: true; + status: object; + }; + }; +} From 91bfbd3794b74617935aaca727f5fe0e9e9b52a3 Mon Sep 17 00:00:00 2001 From: nnamdifrankie Date: Thu, 2 Jan 2020 10:26:27 -0500 Subject: [PATCH 2/9] prefix property with endpoint --- x-pack/plugins/endpoint/server/config.test.ts | 4 ++-- x-pack/plugins/endpoint/server/config.ts | 4 ++-- .../server/services/endpoint/endpoint_query_builders.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/endpoint/server/config.test.ts b/x-pack/plugins/endpoint/server/config.test.ts index 476d84defa1b9c..39f6bca2d43caa 100644 --- a/x-pack/plugins/endpoint/server/config.test.ts +++ b/x-pack/plugins/endpoint/server/config.test.ts @@ -9,7 +9,7 @@ describe('test config schema', () => { it('test config defaults', () => { const config: EndpointConfigType = EndpointConfigSchema.validate({}); expect(config.enabled).toEqual(false); - expect(config.searchResultDefaultPageSize).toEqual(10); - expect(config.searchResultDefaultFirstPageIndex).toEqual(0); + expect(config.endpointResultListDefaultPageSize).toEqual(10); + expect(config.endpointResultListDefaultFirstPageIndex).toEqual(0); }); }); diff --git a/x-pack/plugins/endpoint/server/config.ts b/x-pack/plugins/endpoint/server/config.ts index 87265b1c61f6c0..3f9a8a5508dd8d 100644 --- a/x-pack/plugins/endpoint/server/config.ts +++ b/x-pack/plugins/endpoint/server/config.ts @@ -13,8 +13,8 @@ export type EndpointConfigType = ReturnType extends Observ export const EndpointConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: false }), - searchResultDefaultFirstPageIndex: schema.number({ defaultValue: 0 }), - searchResultDefaultPageSize: schema.number({ defaultValue: 10 }), + endpointResultListDefaultFirstPageIndex: schema.number({ defaultValue: 0 }), + endpointResultListDefaultPageSize: schema.number({ defaultValue: 10 }), }); export function createConfig$(context: PluginInitializerContext) { diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts index 7c4baaa3d145bd..b89e37f6f73b7f 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts @@ -58,8 +58,8 @@ export class AllEndpointsQueryBuilder { async paging() { const config = await this.endpointAppContext.config(); return { - pageSize: this.request.query.pageSize || config.searchResultDefaultPageSize, - pageIndex: this.request.query.pageIndex || config.searchResultDefaultFirstPageIndex, + pageSize: this.request.query.pageSize || config.endpointResultListDefaultPageSize, + pageIndex: this.request.query.pageIndex || config.endpointResultListDefaultFirstPageIndex, }; } } From 6327a4fbdde17b600f8d68d0e721e9f676f6f066 Mon Sep 17 00:00:00 2001 From: nnamdifrankie Date: Thu, 2 Jan 2020 11:59:39 -0500 Subject: [PATCH 3/9] review comments changes --- x-pack/plugins/endpoint/server/routes/endpoints.ts | 4 ++-- .../server/services/endpoint/endpoint_query_builders.ts | 4 ++-- x-pack/plugins/endpoint/server/types.ts | 8 ++++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.ts b/x-pack/plugins/endpoint/server/routes/endpoints.ts index 8cfb4e34e104d3..19efb2aa8a3e19 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.ts +++ b/x-pack/plugins/endpoint/server/routes/endpoints.ts @@ -27,8 +27,8 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp path: '/api/endpoint/endpoints', validate: { query: schema.object({ - pageSize: schema.number({ defaultValue: 10 }), - pageIndex: schema.number({ defaultValue: 0 }), + pageSize: schema.number({ defaultValue: 10, min: 1 }), + pageIndex: schema.number({ defaultValue: 0, min: 0 }), }), }, options: { authRequired: true }, diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts index b89e37f6f73b7f..170fc65bc23c6e 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { KibanaRequest } from 'kibana/server'; -import { EndpointAppContext } from '../../types'; +import { EndpointAppConstants, EndpointAppContext } from '../../types'; export class AllEndpointsQueryBuilder { private readonly request: KibanaRequest; @@ -45,7 +45,7 @@ export class AllEndpointsQueryBuilder { }, from: paging.pageIndex * paging.pageSize, size: paging.pageSize, - index: 'endpoint-agent*', + index: EndpointAppConstants.ENDPOINT_INDEX_NAME, }; } diff --git a/x-pack/plugins/endpoint/server/types.ts b/x-pack/plugins/endpoint/server/types.ts index 5d697d15460f09..c6d0e3dea70cf7 100644 --- a/x-pack/plugins/endpoint/server/types.ts +++ b/x-pack/plugins/endpoint/server/types.ts @@ -11,6 +11,10 @@ export interface EndpointAppContext { config(): Promise; } +export class EndpointAppConstants { + static ENDPOINT_INDEX_NAME = 'endpoint-agent*'; +} + export interface EndpointData { machine_id: string; created_at: Date; @@ -34,7 +38,7 @@ export interface EndpointData { updated_at?: Date; }; isolation: { - status: false; + status: boolean; request_status?: string | boolean; updated_at?: Date; }; @@ -43,7 +47,7 @@ export interface EndpointData { id: string; }; sensor: { - persistence: true; + persistence: boolean; status: object; }; }; From cad4f76c16170273619a478f7d71f06a2123c886 Mon Sep 17 00:00:00 2001 From: nnamdifrankie Date: Thu, 2 Jan 2020 16:14:26 -0500 Subject: [PATCH 4/9] add integration test, add more comments on properties and test --- .../endpoint/server/routes/endpoints.ts | 8 +- .../endpoint/endpoint_query_builders.test.ts | 2 +- .../endpoint/endpoint_query_builders.ts | 2 +- .../apis/endpoint/endpoints.ts | 41 +++++++ .../api_integration/apis/endpoint/index.ts | 1 + .../endpoint/endpoints/data.json.gz | Bin 0 -> 822 bytes .../endpoint/endpoints/mappings.json | 104 ++++++++++++++++++ 7 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 x-pack/test/api_integration/apis/endpoint/endpoints.ts create mode 100644 x-pack/test/functional/es_archives/endpoint/endpoints/data.json.gz create mode 100644 x-pack/test/functional/es_archives/endpoint/endpoints/mappings.json diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.ts b/x-pack/plugins/endpoint/server/routes/endpoints.ts index 19efb2aa8a3e19..a12a5f098351d2 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.ts +++ b/x-pack/plugins/endpoint/server/routes/endpoints.ts @@ -15,9 +15,13 @@ interface HitSource { } 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 requestPageSize: number; + // the index requested requestIndex: number; } @@ -27,7 +31,9 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp path: '/api/endpoint/endpoints', validate: { query: schema.object({ + // the number of results to return for this request per page pageSize: schema.number({ defaultValue: 10, min: 1 }), + // the index of the page to return pageIndex: schema.number({ defaultValue: 0, min: 0 }), }), }, @@ -43,7 +49,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp 'search', queryParams )) as SearchResponse; - return res.ok({ body: await mapToEndpointResultList(queryParams, response) }); + return res.ok({ body: mapToEndpointResultList(queryParams, response) }); } catch (err) { return res.internalError({ body: err }); } diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts index cd0b74db329604..695f17e2ce4d3b 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts @@ -9,7 +9,7 @@ import { AllEndpointsQueryBuilder } from './endpoint_query_builders'; describe('test query builder', () => { describe('test query builder request processing', () => { - it('test query with no filter defaults', async () => { + it('test default query params for all endpoints when no params or body is provided', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {}, }); diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts index 170fc65bc23c6e..18b834822b3120 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts @@ -55,7 +55,7 @@ export class AllEndpointsQueryBuilder { }; } - async paging() { + private async paging() { const config = await this.endpointAppContext.config(); return { pageSize: this.request.query.pageSize || config.endpointResultListDefaultPageSize, diff --git a/x-pack/test/api_integration/apis/endpoint/endpoints.ts b/x-pack/test/api_integration/apis/endpoint/endpoints.ts new file mode 100644 index 00000000000000..3b5dabc6ed84f1 --- /dev/null +++ b/x-pack/test/api_integration/apis/endpoint/endpoints.ts @@ -0,0 +1,41 @@ +/* + * 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 expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + describe('`endpoint` test endpoints api', () => { + before(() => esArchiver.load('endpoint/endpoints')); + after(() => esArchiver.unload('endpoint/endpoints')); + describe('GET /api/endpoint/endpoints', () => { + it('endpoints api should return one entry for each endpoint with default paging', async () => { + const { body } = await supertest + .get('/api/endpoint/endpoints') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + expect(body.total).to.eql(3); + expect(body.endpoints.length).to.eql(3); + expect(body.requestPageSize).to.eql(10); + expect(body.requestIndex).to.eql(0); + }); + + it('endpoints api should return page based on params passed.', async () => { + const { body } = await supertest + .get('/api/endpoint/endpoints?pageSize=1&pageIndex=1') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + expect(body.total).to.eql(3); + expect(body.endpoints.length).to.eql(1); + expect(body.requestPageSize).to.eql(1); + expect(body.requestIndex).to.eql(1); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/endpoint/index.ts b/x-pack/test/api_integration/apis/endpoint/index.ts index e0ffbb13e59787..a3f0e828d7240f 100644 --- a/x-pack/test/api_integration/apis/endpoint/index.ts +++ b/x-pack/test/api_integration/apis/endpoint/index.ts @@ -9,5 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function endpointAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('Endpoint plugin', function() { loadTestFile(require.resolve('./resolver')); + loadTestFile(require.resolve('./endpoints')); }); } diff --git a/x-pack/test/functional/es_archives/endpoint/endpoints/data.json.gz b/x-pack/test/functional/es_archives/endpoint/endpoints/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..fda46096e1ab2465cf93d6c8b09dd111aa85d9f6 GIT binary patch literal 822 zcmV-61Ihd!iwFP!000026YW`DPunmQeebWR^308F$N56yp(`+H54>yw#!KbK4p>T) zEOvkj^}o+4{a#ksI$#AyYL)n2pKD*=9Np7Cbh=$fk4xbkb{$?M&OtM9%d)x|c`y&= zQS!!mj=z^zB>Kb$pT}Q@p*oWzIdyM5axU&0Cz12AkhunSEpk0KtD>%wC7>BeD#5hi z1E!l<1Q7%X10vsWa7g1rj9s6ESF`!uCGHA_8D)u)u&yJF6Y5{z3+&>ccY*c2VxlM}EB1jwHO`-H0W`W_(0Njn)ycjW=Pt(xuAk|>V zlo-g7NssYMS*FCAy~voBMZUI8W2nTBS|~kGn(0~@YhGvB!K%l?vVmz#b;*~xZYC~_ zla<(Ik;!Dd8Tj$&(d>ejulePy#hmBtSfs4Zw9^_=iCh)cX2?=h$|PFk(~8kmE!sD_ zI%(31UzgVv<6<+w?~}MCQ=JWXr>TtfYW$;Gk!9nCk&C_9d;*uyaMeCy$$)WQ38kiv z4159HgMfk`KpL(3{t|T6SZwCXNzKL@_Y3pZ7I&)qRaCd4>Jo%W`yt-V9L*w~Eg3OO zzLB}9?#G((z0^x6hLrFKXo!7aaeyJDB;w6jz~e`>25HinbUF{uC!>qgJtm`OSQR;- z9mr^b@!`m*PkJ$my%RVgQ4F+?deG}p3Q^x9J&NgRFYL9~@rBek-IH5W3+7lp93?$j zF$+rasEdi`5)^D$-x5-=osi60(eoLJ1ySG;KZ1Y{InX4g z5Jw&Y!Td0wQAo&;?7&Fpd(22?N1|d7>_A9!2-_Ul=I99~da6{Gy-|o<>c-wS8VW&} zH#&wC1EUbOHb(P+|3hEo`F@B~k^*5k0m`EkB2Ebqff!@MIEZ+(0}ExZ!a}|kX|^x2 zQtb_;+CS}#URbN)J?12<)hs7jt@h5e+U4mUGm_P6R;yX9_Bu55vSRJ>b$&9UR;<~^ z$ci=lzuI#sDcfU8vRchjlGSSOO{?KOW+bcCEFW3L)=EYgy{S<92_<*7SNSLa0Kz?$ Ad;kCd literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/endpoint/endpoints/mappings.json b/x-pack/test/functional/es_archives/endpoint/endpoints/mappings.json new file mode 100644 index 00000000000000..9544d05d706001 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/endpoints/mappings.json @@ -0,0 +1,104 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "endpoint-agent", + "mappings": { + "properties": { + "created_at": { + "type": "date" + }, + "endpoint": { + "properties": { + "active_directory_distinguished_name": { + "type": "text" + }, + "active_directory_hostname": { + "type": "text" + }, + "domain": { + "type": "text" + }, + "is_base_image": { + "type": "boolean" + }, + "isolation": { + "properties": { + "status": { + "type": "boolean" + } + } + }, + "policy": { + "properties": { + "id": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "sensor": { + "properties": { + "persistence": { + "type": "boolean" + }, + "status": { + "type": "object" + } + } + }, + "upgrade": { + "type": "object" + } + } + }, + "host": { + "properties": { + "hostname": { + "type": "text" + }, + "ip": { + "ignore_above": 256, + "type": "keyword" + }, + "mac_address": { + "type": "text" + }, + "name": { + "type": "text" + }, + "os": { + "properties": { + "full": { + "type": "text" + }, + "name": { + "type": "text" + } + } + } + } + }, + "machine_id": { + "type": "keyword" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file From 42e4f073e851914e1f92ec5222d9c2e511b3e66f Mon Sep 17 00:00:00 2001 From: nnamdifrankie Date: Thu, 2 Jan 2020 19:00:04 -0500 Subject: [PATCH 5/9] add more comments, change query params type --- x-pack/plugins/endpoint/server/routes/endpoints.ts | 10 +++++----- .../services/endpoint/endpoint_query_builders.ts | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.ts b/x-pack/plugins/endpoint/server/routes/endpoints.ts index a12a5f098351d2..ecd33708e23fb9 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.ts +++ b/x-pack/plugins/endpoint/server/routes/endpoints.ts @@ -58,13 +58,13 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp } function mapToEndpointResultList( - queryParams: Record, + queryParams: Record, searchResponse: SearchResponse ): EndpointResultList { if (searchResponse.hits.hits.length > 0) { return { - requestPageSize: queryParams.size as number, - requestIndex: queryParams.from as number, + requestPageSize: queryParams.size, + requestIndex: queryParams.from, endpoints: searchResponse.hits.hits .map(response => response.inner_hits.most_recent.hits.hits) .flatMap(data => data as HitSource) @@ -73,8 +73,8 @@ function mapToEndpointResultList( }; } else { return { - requestPageSize: queryParams.size as number, - requestIndex: queryParams.from as number, + requestPageSize: queryParams.size, + requestIndex: queryParams.from, total: 0, endpoints: [], }; diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts index 18b834822b3120..9df4916f3f555e 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts @@ -14,8 +14,10 @@ export class AllEndpointsQueryBuilder { this.request = request; this.endpointAppContext = endpointAppContext; } - - async toQueryParams(): Promise> { + /* aggregating by endpoint machine id and retrieving the latest of each group of events + related to an endpoint by machine id using elastic search collapse functionality + */ + async toQueryParams(): Promise> { const paging = await this.paging(); return { body: { From 06ba203ff51b1cf89ac7964fdc0c1008f372bf25 Mon Sep 17 00:00:00 2001 From: nnamdifrankie Date: Fri, 3 Jan 2020 09:59:39 -0500 Subject: [PATCH 6/9] remove accent from description --- x-pack/test/api_integration/apis/endpoint/endpoints.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/endpoint/endpoints.ts b/x-pack/test/api_integration/apis/endpoint/endpoints.ts index 3b5dabc6ed84f1..a7f8b58fabf7b1 100644 --- a/x-pack/test/api_integration/apis/endpoint/endpoints.ts +++ b/x-pack/test/api_integration/apis/endpoint/endpoints.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('`endpoint` test endpoints api', () => { + describe('test endpoints api', () => { before(() => esArchiver.load('endpoint/endpoints')); after(() => esArchiver.unload('endpoint/endpoints')); describe('GET /api/endpoint/endpoints', () => { From 18aaa7b15f5b4c6e9b2527313408133970e762c4 Mon Sep 17 00:00:00 2001 From: nnamdifrankie Date: Mon, 6 Jan 2020 15:04:21 -0500 Subject: [PATCH 7/9] add more testing, use more function --- .../endpoint/server/routes/endpoints.test.ts | 88 +++------------ .../endpoint/server/routes/endpoints.ts | 36 +++--- .../endpoint/endpoint_query_builders.test.ts | 5 +- .../endpoint/endpoint_query_builders.ts | 103 +++++++++--------- .../apis/endpoint/endpoints.ts | 33 +++++- 5 files changed, 119 insertions(+), 146 deletions(-) diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts index 4ba020bdfb0738..5bbbbc26dfa2f4 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts +++ b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts @@ -47,16 +47,13 @@ describe('test endpoint route', () => { }); it('test find the latest of all endpoints', async () => { - const mockRequest = httpServerMock.createKibanaRequest({ - body: {}, - params: {}, - }); + const mockRequest = httpServerMock.createKibanaRequest({}); const response: SearchResponse = (data as unknown) as SearchResponse< EndpointData >; mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/endpoints') )!; @@ -72,38 +69,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalledWith('search', { - from: 0, - size: 10, - body: { - query: { - match_all: {}, - }, - collapse: { - field: 'machine_id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ created_at: 'desc' }], - }, - }, - aggs: { - total: { - cardinality: { - field: 'machine_id', - }, - }, - }, - sort: [ - { - created_at: { - order: 'desc', - }, - }, - ], - }, - index: 'endpoint-agent*', - }); + 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; @@ -115,16 +81,21 @@ describe('test endpoint route', () => { it('test find the latest of all endpoints with params', async () => { const mockRequest = httpServerMock.createKibanaRequest({ - body: {}, - query: { - pageSize: 10, - pageIndex: 1, + body: { + pagingProperties: [ + { + pageSize: 10, + }, + { + pageIndex: 1, + }, + ], }, }); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve((data as unknown) as SearchResponse) ); - [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/endpoints') )!; @@ -140,38 +111,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalledWith('search', { - from: 10, - size: 10, - body: { - query: { - match_all: {}, - }, - collapse: { - field: 'machine_id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ created_at: 'desc' }], - }, - }, - aggs: { - total: { - cardinality: { - field: 'machine_id', - }, - }, - }, - sort: [ - { - created_at: { - order: 'desc', - }, - }, - ], - }, - index: 'endpoint-agent*', - }); + 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; diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.ts b/x-pack/plugins/endpoint/server/routes/endpoints.ts index ecd33708e23fb9..40f86e26de60da 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.ts +++ b/x-pack/plugins/endpoint/server/routes/endpoints.ts @@ -8,7 +8,8 @@ import { IRouter } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; import { EndpointAppContext, EndpointData } from '../types'; -import { AllEndpointsQueryBuilder } from '../services/endpoint/endpoint_query_builders'; +import { kibanaRequestToEndpointListQuery } from '../services/endpoint/endpoint_query_builders'; +import { RouteSchemas } from '../../../../../build/kibana/src/core/server/http/router/route'; interface HitSource { _source: EndpointData; @@ -25,26 +26,33 @@ export interface EndpointResultList { requestIndex: number; } +const endpointListRequestSchema: RouteSchemas = { + body: schema.nullable( + schema.object({ + pagingProperties: schema.arrayOf( + schema.oneOf([ + // the number of results to return for this request per page + schema.object({ + pageSize: schema.number({ defaultValue: 10, min: 1, max: 10000 }), + }), + // the index of the page to return + schema.object({ pageIndex: schema.number({ defaultValue: 0, min: 0 }) }), + ]) + ), + }) + ), +}; + export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { - router.get( + router.post( { path: '/api/endpoint/endpoints', - validate: { - query: schema.object({ - // the number of results to return for this request per page - pageSize: schema.number({ defaultValue: 10, min: 1 }), - // the index of the page to return - pageIndex: schema.number({ defaultValue: 0, min: 0 }), - }), - }, + validate: endpointListRequestSchema, options: { authRequired: true }, }, async (context, req, res) => { try { - const queryParams = await new AllEndpointsQueryBuilder( - req, - endpointAppContext - ).toQueryParams(); + const queryParams = await kibanaRequestToEndpointListQuery(req, endpointAppContext); const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( 'search', queryParams diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts index 695f17e2ce4d3b..2a8cecec16526b 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts @@ -5,7 +5,7 @@ */ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; import { EndpointConfigSchema } from '../../config'; -import { AllEndpointsQueryBuilder } from './endpoint_query_builders'; +import { kibanaRequestToEndpointListQuery } from './endpoint_query_builders'; describe('test query builder', () => { describe('test query builder request processing', () => { @@ -13,11 +13,10 @@ describe('test query builder', () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {}, }); - const searchQueryBuilder = new AllEndpointsQueryBuilder(mockRequest, { + const query = await kibanaRequestToEndpointListQuery(mockRequest, { logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); - const query = await searchQueryBuilder.toQueryParams(); expect(query).toEqual({ body: { query: { diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts index 9df4916f3f555e..f69a72d1427e5f 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts @@ -6,62 +6,61 @@ import { KibanaRequest } from 'kibana/server'; import { EndpointAppConstants, EndpointAppContext } from '../../types'; -export class AllEndpointsQueryBuilder { - private readonly request: KibanaRequest; - private readonly endpointAppContext: EndpointAppContext; - - constructor(request: KibanaRequest, endpointAppContext: EndpointAppContext) { - this.request = request; - this.endpointAppContext = endpointAppContext; - } - /* aggregating by endpoint machine id and retrieving the latest of each group of events - related to an endpoint by machine id using elastic search collapse functionality - */ - async toQueryParams(): Promise> { - const paging = await this.paging(); - return { - body: { - query: this.queryBody(), - collapse: { - field: 'machine_id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ created_at: 'desc' }], - }, +export const kibanaRequestToEndpointListQuery = async ( + request: KibanaRequest, + endpointAppContext: EndpointAppContext +): Promise> => { + const pagingProperties = await getPagingProperties(request, endpointAppContext); + return { + body: { + query: { + match_all: {}, + }, + collapse: { + field: 'machine_id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ created_at: 'desc' }], }, - aggs: { - total: { - cardinality: { - field: 'machine_id', - }, + }, + aggs: { + total: { + cardinality: { + field: 'machine_id', }, }, - sort: [ - { - created_at: { - order: 'desc', - }, - }, - ], }, - from: paging.pageIndex * paging.pageSize, - size: paging.pageSize, - index: EndpointAppConstants.ENDPOINT_INDEX_NAME, - }; - } - - private queryBody(): Record { - return { - match_all: {}, - }; - } + sort: [ + { + created_at: { + order: 'desc', + }, + }, + ], + }, + from: pagingProperties.pageIndex * pagingProperties.pageSize, + size: pagingProperties.pageSize, + index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + }; +}; - private async paging() { - const config = await this.endpointAppContext.config(); - return { - pageSize: this.request.query.pageSize || config.endpointResultListDefaultPageSize, - pageIndex: this.request.query.pageIndex || config.endpointResultListDefaultFirstPageIndex, - }; +async function getPagingProperties( + request: KibanaRequest, + endpointAppContext: EndpointAppContext +) { + const config = await endpointAppContext.config(); + const pagingProperties: { pageSize?: number; pageIndex?: number } = {}; + if (request?.body?.pagingProperties) { + for (const property of request.body.pagingProperties) { + Object.assign( + pagingProperties, + ...Object.keys(property).map(key => ({ [key]: property[key] })) + ); + } } + return { + pageSize: pagingProperties.pageSize || config.endpointResultListDefaultPageSize, + pageIndex: pagingProperties.pageIndex || config.endpointResultListDefaultFirstPageIndex, + }; } diff --git a/x-pack/test/api_integration/apis/endpoint/endpoints.ts b/x-pack/test/api_integration/apis/endpoint/endpoints.ts index a7f8b58fabf7b1..64df33c28a2db6 100644 --- a/x-pack/test/api_integration/apis/endpoint/endpoints.ts +++ b/x-pack/test/api_integration/apis/endpoint/endpoints.ts @@ -15,7 +15,7 @@ export default function({ getService }: FtrProviderContext) { describe('GET /api/endpoint/endpoints', () => { it('endpoints api should return one entry for each endpoint with default paging', async () => { const { body } = await supertest - .get('/api/endpoint/endpoints') + .post('/api/endpoint/endpoints') .set('kbn-xsrf', 'xxx') .send() .expect(200); @@ -27,15 +27,42 @@ export default function({ getService }: FtrProviderContext) { it('endpoints api should return page based on params passed.', async () => { const { body } = await supertest - .get('/api/endpoint/endpoints?pageSize=1&pageIndex=1') + .post('/api/endpoint/endpoints') .set('kbn-xsrf', 'xxx') - .send() + .send({ + pagingProperties: [ + { + pageSize: 1, + }, + { + pageIndex: 1, + }, + ], + }) .expect(200); expect(body.total).to.eql(3); expect(body.endpoints.length).to.eql(1); expect(body.requestPageSize).to.eql(1); expect(body.requestIndex).to.eql(1); }); + + it('endpoints api should return 400 when pagingProperties is below boundaries.', async () => { + const { body } = await supertest + .post('/api/endpoint/endpoints') + .set('kbn-xsrf', 'xxx') + .send({ + pagingProperties: [ + { + pageSize: 0, + }, + { + pageIndex: 1, + }, + ], + }) + .expect(400); + expect(body.message).to.contain('Value is [0] but it must be equal to or greater than [1]'); + }); }); }); } From b8ed0e8eb779b09e9a9010b3ccb651796be53cd7 Mon Sep 17 00:00:00 2001 From: nnamdifrankie Date: Mon, 6 Jan 2020 16:24:43 -0500 Subject: [PATCH 8/9] fix type check --- x-pack/plugins/endpoint/server/routes/endpoints.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.ts b/x-pack/plugins/endpoint/server/routes/endpoints.ts index 40f86e26de60da..828bacfb725e9e 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.ts +++ b/x-pack/plugins/endpoint/server/routes/endpoints.ts @@ -9,7 +9,8 @@ import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; import { EndpointAppContext, EndpointData } from '../types'; import { kibanaRequestToEndpointListQuery } from '../services/endpoint/endpoint_query_builders'; -import { RouteSchemas } from '../../../../../build/kibana/src/core/server/http/router/route'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { RouteSchemas } from '../../../../../src/core/server/http/router/route'; interface HitSource { _source: EndpointData; From 0686689705ff56dc9be73cc623ecf895cb0f0e29 Mon Sep 17 00:00:00 2001 From: nnamdifrankie Date: Mon, 6 Jan 2020 17:32:54 -0500 Subject: [PATCH 9/9] fix type check, fix api variable case --- .../endpoint/server/routes/endpoints.test.ts | 14 +++--- .../endpoint/server/routes/endpoints.ts | 48 +++++++++---------- .../endpoint/endpoint_query_builders.ts | 10 ++-- .../apis/endpoint/endpoints.ts | 20 ++++---- 4 files changed, 44 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts index 5bbbbc26dfa2f4..60433f86b6f7ed 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts +++ b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts @@ -75,19 +75,19 @@ describe('test endpoint route', () => { const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as EndpointResultList; expect(endpointResultList.endpoints.length).toEqual(3); expect(endpointResultList.total).toEqual(3); - expect(endpointResultList.requestIndex).toEqual(0); - expect(endpointResultList.requestPageSize).toEqual(10); + 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: { - pagingProperties: [ + paging_properties: [ { - pageSize: 10, + page_size: 10, }, { - pageIndex: 1, + page_index: 1, }, ], }, @@ -117,7 +117,7 @@ describe('test endpoint route', () => { const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as EndpointResultList; expect(endpointResultList.endpoints.length).toEqual(3); expect(endpointResultList.total).toEqual(3); - expect(endpointResultList.requestIndex).toEqual(10); - expect(endpointResultList.requestPageSize).toEqual(10); + expect(endpointResultList.request_index).toEqual(10); + expect(endpointResultList.request_page_size).toEqual(10); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.ts b/x-pack/plugins/endpoint/server/routes/endpoints.ts index 828bacfb725e9e..59430947d97da7 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.ts +++ b/x-pack/plugins/endpoint/server/routes/endpoints.ts @@ -9,8 +9,6 @@ import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; import { EndpointAppContext, EndpointData } from '../types'; import { kibanaRequestToEndpointListQuery } from '../services/endpoint/endpoint_query_builders'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RouteSchemas } from '../../../../../src/core/server/http/router/route'; interface HitSource { _source: EndpointData; @@ -22,33 +20,31 @@ export interface EndpointResultList { // the total number of unique endpoints in the index total: number; // the page size requested - requestPageSize: number; + request_page_size: number; // the index requested - requestIndex: number; + request_index: number; } -const endpointListRequestSchema: RouteSchemas = { - body: schema.nullable( - schema.object({ - pagingProperties: schema.arrayOf( - schema.oneOf([ - // the number of results to return for this request per page - schema.object({ - pageSize: schema.number({ defaultValue: 10, min: 1, max: 10000 }), - }), - // the index of the page to return - schema.object({ pageIndex: schema.number({ defaultValue: 0, min: 0 }) }), - ]) - ), - }) - ), -}; - export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { router.post( { path: '/api/endpoint/endpoints', - validate: endpointListRequestSchema, + 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) => { @@ -72,8 +68,8 @@ function mapToEndpointResultList( ): EndpointResultList { if (searchResponse.hits.hits.length > 0) { return { - requestPageSize: queryParams.size, - requestIndex: queryParams.from, + 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) @@ -82,8 +78,8 @@ function mapToEndpointResultList( }; } else { return { - requestPageSize: queryParams.size, - requestIndex: queryParams.from, + request_page_size: queryParams.size, + request_index: queryParams.from, total: 0, endpoints: [], }; diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts index f69a72d1427e5f..7430ba97216083 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts @@ -50,9 +50,9 @@ async function getPagingProperties( endpointAppContext: EndpointAppContext ) { const config = await endpointAppContext.config(); - const pagingProperties: { pageSize?: number; pageIndex?: number } = {}; - if (request?.body?.pagingProperties) { - for (const property of request.body.pagingProperties) { + const pagingProperties: { page_size?: number; page_index?: number } = {}; + if (request?.body?.paging_properties) { + for (const property of request.body.paging_properties) { Object.assign( pagingProperties, ...Object.keys(property).map(key => ({ [key]: property[key] })) @@ -60,7 +60,7 @@ async function getPagingProperties( } } return { - pageSize: pagingProperties.pageSize || config.endpointResultListDefaultPageSize, - pageIndex: pagingProperties.pageIndex || config.endpointResultListDefaultFirstPageIndex, + pageSize: pagingProperties.page_size || config.endpointResultListDefaultPageSize, + pageIndex: pagingProperties.page_index || config.endpointResultListDefaultFirstPageIndex, }; } diff --git a/x-pack/test/api_integration/apis/endpoint/endpoints.ts b/x-pack/test/api_integration/apis/endpoint/endpoints.ts index 64df33c28a2db6..95c3678672da30 100644 --- a/x-pack/test/api_integration/apis/endpoint/endpoints.ts +++ b/x-pack/test/api_integration/apis/endpoint/endpoints.ts @@ -21,8 +21,8 @@ export default function({ getService }: FtrProviderContext) { .expect(200); expect(body.total).to.eql(3); expect(body.endpoints.length).to.eql(3); - expect(body.requestPageSize).to.eql(10); - expect(body.requestIndex).to.eql(0); + expect(body.request_page_size).to.eql(10); + expect(body.request_index).to.eql(0); }); it('endpoints api should return page based on params passed.', async () => { @@ -30,20 +30,20 @@ export default function({ getService }: FtrProviderContext) { .post('/api/endpoint/endpoints') .set('kbn-xsrf', 'xxx') .send({ - pagingProperties: [ + paging_properties: [ { - pageSize: 1, + page_size: 1, }, { - pageIndex: 1, + page_index: 1, }, ], }) .expect(200); expect(body.total).to.eql(3); expect(body.endpoints.length).to.eql(1); - expect(body.requestPageSize).to.eql(1); - expect(body.requestIndex).to.eql(1); + expect(body.request_page_size).to.eql(1); + expect(body.request_index).to.eql(1); }); it('endpoints api should return 400 when pagingProperties is below boundaries.', async () => { @@ -51,12 +51,12 @@ export default function({ getService }: FtrProviderContext) { .post('/api/endpoint/endpoints') .set('kbn-xsrf', 'xxx') .send({ - pagingProperties: [ + paging_properties: [ { - pageSize: 0, + page_size: 0, }, { - pageIndex: 1, + page_index: 1, }, ], })