Skip to content

Commit

Permalink
EMT-issue-65: add endpoint list api (#53861)
Browse files Browse the repository at this point in the history
add endpoint list api
  • Loading branch information
nnamdifrankie authored Jan 7, 2020
1 parent f46e8e2 commit 6a2fb61
Show file tree
Hide file tree
Showing 15 changed files with 1,009 additions and 7 deletions.
15 changes: 15 additions & 0 deletions x-pack/plugins/endpoint/server/config.test.ts
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);
});
});
22 changes: 22 additions & 0 deletions x-pack/plugins/endpoint/server/config.ts
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>>();
}
8 changes: 4 additions & 4 deletions x-pack/plugins/endpoint/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@
* 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,
EndpointPluginSetup,
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<
EndpointPluginSetup,
EndpointPluginStart,
EndpointPluginSetupDependencies,
EndpointPluginStartDependencies
> = () => new EndpointPlugin();
> = (initializerContext: PluginInitializerContext) => new EndpointPlugin(initializerContext);
39 changes: 39 additions & 0 deletions x-pack/plugins/endpoint/server/plugin.test.ts
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);
});
});
27 changes: 24 additions & 3 deletions x-pack/plugins/endpoint/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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',
Expand All @@ -49,10 +57,23 @@ export class EndpointPlugin
},
},
});
const endpointContext = {
logFactory: this.initializerContext.logger,
config: (): Promise<EndpointConfigType> => {
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');
}
}
123 changes: 123 additions & 0 deletions x-pack/plugins/endpoint/server/routes/endpoints.test.ts
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);
});
});
87 changes: 87 additions & 0 deletions x-pack/plugins/endpoint/server/routes/endpoints.ts
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: [],
};
}
}
Loading

0 comments on commit 6a2fb61

Please sign in to comment.