Skip to content

Commit

Permalink
EMT-146: use ingest agent for status info
Browse files Browse the repository at this point in the history
  • Loading branch information
nnamdifrankie committed Apr 18, 2020
1 parent 675c589 commit ad9dad3
Show file tree
Hide file tree
Showing 13 changed files with 207 additions and 32 deletions.
17 changes: 16 additions & 1 deletion x-pack/plugins/endpoint/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { IngestManagerSetupContract } from '../../ingest_manager/server';
import { AgentService } from '../../ingest_manager/common/types';

/**
* Creates a mock IndexPatternRetriever for use in tests.
*
Expand All @@ -28,17 +31,29 @@ export const createMockMetadataIndexPatternRetriever = () => {
return createMockIndexPatternRetriever(MetadataIndexPattern);
};

/**
* Creates a mock AgentService
*/
export const createMockAgentService = (): jest.Mocked<AgentService> => {
return {
getAgentStatus: jest.fn(),
};
};

/**
* Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's
* ESIndexPatternService.
*
* @param indexPattern a string index pattern to return when called by a test
* @returns the same value as `indexPattern` parameter
*/
export const createMockIndexPatternService = (indexPattern: string) => {
export const createMockIngestManagerSetupContract = (
indexPattern: string
): IngestManagerSetupContract => {
return {
esIndexPatternService: {
getESIndexPattern: jest.fn().mockResolvedValue(indexPattern),
},
agentService: createMockAgentService(),
};
};
4 changes: 2 additions & 2 deletions x-pack/plugins/endpoint/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin';
import { coreMock } from '../../../../src/core/server/mocks';
import { PluginSetupContract } from '../../features/server';
import { createMockIndexPatternService } from './mocks';
import { createMockIngestManagerSetupContract } from './mocks';

describe('test endpoint plugin', () => {
let plugin: EndpointPlugin;
Expand All @@ -31,7 +31,7 @@ describe('test endpoint plugin', () => {
};
mockedEndpointPluginSetupDependencies = {
features: mockedPluginSetupContract,
ingestManager: createMockIndexPatternService(''),
ingestManager: createMockIngestManagerSetupContract(''),
};
});

Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/endpoint/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class EndpointPlugin
plugins.ingestManager.esIndexPatternService,
this.initializerContext.logger
),
agentService: plugins.ingestManager.agentService,
logFactory: this.initializerContext.logger,
config: (): Promise<EndpointConfigType> => {
return createConfig$(this.initializerContext)
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { registerAlertRoutes } from './index';
import { EndpointConfigSchema } from '../../config';
import { alertingIndexGetQuerySchema } from '../../../common/schema/alert_index';
import { createMockIndexPatternRetriever } from '../../mocks';
import { createMockAgentService, createMockIndexPatternRetriever } from '../../mocks';

describe('test alerts route', () => {
let routerMock: jest.Mocked<IRouter>;
Expand All @@ -26,6 +26,7 @@ describe('test alerts route', () => {
routerMock = httpServiceMock.createRouter();
registerAlertRoutes(routerMock, {
indexPatternRetriever: createMockIndexPatternRetriever('events-endpoint-*'),
agentService: createMockAgentService(),
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ export const alertDetailsHandlerWrapper = function(
indexPattern
);

const currentHostInfo = await getHostData(ctx, response._source.host.id, indexPattern);
const currentHostInfo = await getHostData(
{
endpointAppContext,
requestHandlerContext: ctx,
},
response._source.host.id
);

return res.ok({
body: {
Expand Down
88 changes: 67 additions & 21 deletions x-pack/plugins/endpoint/server/routes/metadata/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { IRouter, RequestHandlerContext } from 'kibana/server';
import { IRouter, Logger, RequestHandlerContext } from 'kibana/server';
import { SearchResponse } from 'elasticsearch';
import { schema } from '@kbn/config-schema';

import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders';
import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders';
import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types';
import { EndpointAppContext } from '../../types';
import { AgentStatus } from '../../../../ingest_manager/common/types/models';

interface HitSource {
_source: HostMetadata;
}

interface MetadataRequestContext {
requestHandlerContext: RequestHandlerContext;
endpointAppContext: EndpointAppContext;
}

const HOST_STATUS_MAPPING = new Map<AgentStatus, HostStatus>([
['online', HostStatus.ONLINE],
['offline', HostStatus.OFFLINE],
]);

export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) {
router.post(
{
Expand Down Expand Up @@ -62,7 +73,12 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
'search',
queryParams
)) as SearchResponse<HostMetadata>;
return res.ok({ body: mapToHostResultList(queryParams, response) });
return res.ok({
body: await mapToHostResultList(queryParams, response, {
endpointAppContext,
requestHandlerContext: context,
}),
});
} catch (err) {
return res.internalError({ body: err });
}
Expand All @@ -79,11 +95,13 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
},
async (context, req, res) => {
try {
const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern(
context
const doc = await getHostData(
{
endpointAppContext,
requestHandlerContext: context,
},
req.params.id
);

const doc = await getHostData(context, req.params.id, index);
if (doc) {
return res.ok({ body: doc });
}
Expand All @@ -96,12 +114,14 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
}

export async function getHostData(
context: RequestHandlerContext,
id: string,
index: string
metadataRequestContext: MetadataRequestContext,
id: string
): Promise<HostInfo | undefined> {
const index = await metadataRequestContext.endpointAppContext.indexPatternRetriever.getMetadataIndexPattern(
metadataRequestContext.requestHandlerContext
);
const query = getESQueryHostMetadataByID(id, index);
const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser(
const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.dataClient.callAsCurrentUser(
'search',
query
)) as SearchResponse<HostMetadata>;
Expand All @@ -110,22 +130,25 @@ export async function getHostData(
return undefined;
}

return enrichHostMetadata(response.hits.hits[0]._source);
return await enrichHostMetadata(response.hits.hits[0]._source, metadataRequestContext);
}

function mapToHostResultList(
async function mapToHostResultList(
queryParams: Record<string, any>,
searchResponse: SearchResponse<HostMetadata>
): HostResultList {
searchResponse: SearchResponse<HostMetadata>,
metadataRequestContext: MetadataRequestContext
): Promise<HostResultList> {
const totalNumberOfHosts = searchResponse?.aggregations?.total?.value || 0;
if (searchResponse.hits.hits.length > 0) {
return {
request_page_size: queryParams.size,
request_page_index: queryParams.from,
hosts: searchResponse.hits.hits
.map(response => response.inner_hits.most_recent.hits.hits)
.flatMap(data => data as HitSource)
.map(entry => enrichHostMetadata(entry._source)),
hosts: await Promise.all(
searchResponse.hits.hits
.map(response => response.inner_hits.most_recent.hits.hits)
.flatMap(data => data as HitSource)
.map(async entry => enrichHostMetadata(entry._source, metadataRequestContext))
),
total: totalNumberOfHosts,
};
} else {
Expand All @@ -138,9 +161,32 @@ function mapToHostResultList(
}
}

function enrichHostMetadata(hostMetadata: HostMetadata): HostInfo {
async function enrichHostMetadata(
hostMetadata: HostMetadata,
metadataRequestContext: MetadataRequestContext
): Promise<HostInfo> {
let hostStatus = HostStatus.ERROR;
try {
const status = await metadataRequestContext.endpointAppContext.agentService.getAgentStatus(
metadataRequestContext.requestHandlerContext,
hostMetadata.elastic.agent.id
);
hostStatus = HOST_STATUS_MAPPING.get(status) || HostStatus.ERROR;
} catch (e) {
if (e.isBoom && e.output.statusCode === 404) {
logger(metadataRequestContext.endpointAppContext).warn(
`agent with id ${hostMetadata.elastic.agent.id} not found`
);
} else {
throw e;
}
}
return {
metadata: hostMetadata,
host_status: HostStatus.ERROR,
host_status: hostStatus,
};
}

const logger = (endpointAppContext: EndpointAppContext): Logger => {
return endpointAppContext.logFactory.get('metadata');
};
72 changes: 68 additions & 4 deletions x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import { SearchResponse } from 'elasticsearch';
import { registerEndpointRoutes } from './index';
import { EndpointConfigSchema } from '../../config';
import * as data from '../../test_data/all_metadata_data.json';
import { createMockMetadataIndexPatternRetriever } from '../../mocks';
import { createMockAgentService, createMockMetadataIndexPatternRetriever } from '../../mocks';
import { AgentService } from '../../../../ingest_manager/common/types';
import Boom from 'boom';

describe('test endpoint route', () => {
let routerMock: jest.Mocked<IRouter>;
Expand All @@ -35,6 +37,7 @@ describe('test endpoint route', () => {
let mockSavedObjectClient: jest.Mocked<SavedObjectsClientContract>;
let routeHandler: RequestHandler<any, any, any>;
let routeConfig: RouteConfig<any, any, any, any>;
let mockAgentService: jest.Mocked<AgentService>;

beforeEach(() => {
mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked<
Expand All @@ -45,8 +48,10 @@ describe('test endpoint route', () => {
mockClusterClient.asScoped.mockReturnValue(mockScopedClient);
routerMock = httpServiceMock.createRouter();
mockResponse = httpServerMock.createResponseFactory();
mockAgentService = createMockAgentService();
registerEndpointRoutes(routerMock, {
indexPatternRetriever: createMockMetadataIndexPatternRetriever(),
agentService: mockAgentService,
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
});
Expand Down Expand Up @@ -83,7 +88,7 @@ describe('test endpoint route', () => {
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;

mockAgentService.getAgentStatus = jest.fn().mockReturnValue('error');
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
Expand Down Expand Up @@ -113,6 +118,8 @@ describe('test endpoint route', () => {
],
},
});

mockAgentService.getAgentStatus = jest.fn().mockReturnValue('error');
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve((data as unknown) as SearchResponse<HostMetadata>)
);
Expand Down Expand Up @@ -154,6 +161,8 @@ describe('test endpoint route', () => {
filter: 'not host.ip:10.140.73.246',
},
});

mockAgentService.getAgentStatus = jest.fn().mockReturnValue('error');
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve((data as unknown) as SearchResponse<HostMetadata>)
);
Expand Down Expand Up @@ -216,10 +225,10 @@ describe('test endpoint route', () => {
},
})
);
mockAgentService.getAgentStatus = jest.fn().mockReturnValue('error');
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;

await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
Expand All @@ -233,13 +242,14 @@ describe('test endpoint route', () => {
expect(message).toEqual('Endpoint Not Found');
});

it('should return a single endpoint with status error', async () => {
it('should return a single endpoint with status online', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: (data as any).hits.hits[0]._id },
});
const response: SearchResponse<HostMetadata> = (data as unknown) as SearchResponse<
HostMetadata
>;
mockAgentService.getAgentStatus = jest.fn().mockReturnValue('online');
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
Expand All @@ -256,6 +266,60 @@ describe('test endpoint route', () => {
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result).toHaveProperty('metadata.endpoint');
expect(result.host_status).toEqual(HostStatus.ONLINE);
});

it('should return a single endpoint with status error when AgentService throw 404', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: (data as any).hits.hits[0]._id },
});
const response: SearchResponse<HostMetadata> = (data as unknown) as SearchResponse<
HostMetadata
>;
mockAgentService.getAgentStatus = jest.fn().mockImplementation(() => {
throw Boom.notFound('Agent not found');
});
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;

await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);

expect(mockScopedClient.callAsCurrentUser).toBeCalled();
expect(routeConfig.options).toEqual({ authRequired: true });
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result.host_status).toEqual(HostStatus.ERROR);
});

it('should return a single endpoint with status error when status is not offline or online', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: (data as any).hits.hits[0]._id },
});
const response: SearchResponse<HostMetadata> = (data as unknown) as SearchResponse<
HostMetadata
>;
mockAgentService.getAgentStatus = jest.fn().mockReturnValue('warning');
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;

await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);

expect(mockScopedClient.callAsCurrentUser).toBeCalled();
expect(routeConfig.options).toEqual({ authRequired: true });
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result.host_status).toEqual(HostStatus.ERROR);
});
});
Expand Down
Loading

0 comments on commit ad9dad3

Please sign in to comment.