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..39f6bca2d43caa --- /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.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 new file mode 100644 index 00000000000000..3f9a8a5508dd8d --- /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 }), + endpointResultListDefaultFirstPageIndex: schema.number({ defaultValue: 0 }), + endpointResultListDefaultPageSize: 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..60433f86b6f7ed --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts @@ -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; + 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({}); + + const response: SearchResponse = (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) + ); + [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); + }); +}); 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..59430947d97da7 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/endpoints.ts @@ -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; + return res.ok({ body: mapToEndpointResultList(queryParams, response) }); + } catch (err) { + return res.internalError({ body: err }); + } + } + ); +} + +function mapToEndpointResultList( + queryParams: Record, + searchResponse: SearchResponse +): 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: [], + }; + } +} 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..2a8cecec16526b --- /dev/null +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { kibanaRequestToEndpointListQuery } from './endpoint_query_builders'; + +describe('test query builder', () => { + describe('test query builder request processing', () => { + it('test default query params for all endpoints when no params or body is provided', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + body: {}, + }); + const query = await kibanaRequestToEndpointListQuery(mockRequest, { + logFactory: loggingServiceMock.create(), + config: () => Promise.resolve(EndpointConfigSchema.validate({})), + }); + 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..7430ba97216083 --- /dev/null +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts @@ -0,0 +1,66 @@ +/* + * 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 { EndpointAppConstants, EndpointAppContext } from '../../types'; + +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', + }, + }, + }, + sort: [ + { + created_at: { + order: 'desc', + }, + }, + ], + }, + from: pagingProperties.pageIndex * pagingProperties.pageSize, + size: pagingProperties.pageSize, + index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + }; +}; + +async function getPagingProperties( + request: KibanaRequest, + endpointAppContext: EndpointAppContext +) { + const config = await endpointAppContext.config(); + 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] })) + ); + } + } + return { + pageSize: pagingProperties.page_size || config.endpointResultListDefaultPageSize, + pageIndex: pagingProperties.page_index || config.endpointResultListDefaultFirstPageIndex, + }; +} 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..c6d0e3dea70cf7 --- /dev/null +++ b/x-pack/plugins/endpoint/server/types.ts @@ -0,0 +1,54 @@ +/* + * 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 class EndpointAppConstants { + static ENDPOINT_INDEX_NAME = 'endpoint-agent*'; +} + +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: boolean; + request_status?: string | boolean; + updated_at?: Date; + }; + policy: { + name: string; + id: string; + }; + sensor: { + persistence: boolean; + status: object; + }; + }; +} 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..95c3678672da30 --- /dev/null +++ b/x-pack/test/api_integration/apis/endpoint/endpoints.ts @@ -0,0 +1,68 @@ +/* + * 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('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 + .post('/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.request_page_size).to.eql(10); + expect(body.request_index).to.eql(0); + }); + + it('endpoints api should return page based on params passed.', async () => { + const { body } = await supertest + .post('/api/endpoint/endpoints') + .set('kbn-xsrf', 'xxx') + .send({ + paging_properties: [ + { + page_size: 1, + }, + { + page_index: 1, + }, + ], + }) + .expect(200); + expect(body.total).to.eql(3); + expect(body.endpoints.length).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 () => { + const { body } = await supertest + .post('/api/endpoint/endpoints') + .set('kbn-xsrf', 'xxx') + .send({ + paging_properties: [ + { + page_size: 0, + }, + { + page_index: 1, + }, + ], + }) + .expect(400); + expect(body.message).to.contain('Value is [0] but it must be equal to or greater than [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 00000000000000..fda46096e1ab24 Binary files /dev/null and b/x-pack/test/functional/es_archives/endpoint/endpoints/data.json.gz differ 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