Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
getAgentStatusById: 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
99 changes: 78 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,43 @@ function mapToHostResultList(
}
}

function enrichHostMetadata(hostMetadata: HostMetadata): HostInfo {
async function enrichHostMetadata(
hostMetadata: HostMetadata,
metadataRequestContext: MetadataRequestContext
): Promise<HostInfo> {
let hostStatus = HostStatus.ERROR;
let elasticAgentId = hostMetadata?.elastic?.agent?.id;
const log = logger(metadataRequestContext.endpointAppContext);
try {
/**
* Get agent status by elastic agent id if available or use the host id.
* https://github.com/elastic/endpoint-app-team/issues/354
*/

if (!elasticAgentId) {
elasticAgentId = hostMetadata.host.id;
log.warn(`Missing elastic agent id, using host id instead ${elasticAgentId}`);
}

const status = await metadataRequestContext.endpointAppContext.agentService.getAgentStatusById(
metadataRequestContext.requestHandlerContext.core.savedObjects.client,
elasticAgentId
);
hostStatus = HOST_STATUS_MAPPING.get(status) || HostStatus.ERROR;
} catch (e) {
if (e.isBoom && e.output.statusCode === 404) {
log.warn(`agent with id ${elasticAgentId} not found`);
} else {
log.error(e);
throw e;
}
}
return {
metadata: hostMetadata,
host_status: HostStatus.ERROR,
host_status: hostStatus,
};
}

const logger = (endpointAppContext: EndpointAppContext): Logger => {
return endpointAppContext.logFactory.get('metadata');
};
Loading