Skip to content

Commit

Permalink
[SECURITY-ENDPOINT] use ingest manager unenroll services to remove un…
Browse files Browse the repository at this point in the history
…enrolled endpoint (#70393)

[SECURITY-ENDPOINT] EMT-451 - use ingest manager unenroll services to remove unenrolled endpoint
  • Loading branch information
nnamdifrankie authored Jul 1, 2020
1 parent 80ae564 commit eedb5f7
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 367 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@
export const eventsIndexPattern = 'logs-endpoint.events.*';
export const alertsIndexPattern = 'logs-endpoint.alerts-*';
export const metadataIndexPattern = 'metrics-endpoint.metadata-*';
export const metadataMirrorIndexPattern = 'metrics-endpoint.metadata_mirror-*';
export const policyIndexPattern = 'metrics-endpoint.policy-*';
export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import {
HostStatus,
} from '../../../../common/endpoint/types';
import { EndpointAppContext } from '../../types';
import { AgentStatus } from '../../../../../ingest_manager/common/types/models';
import { findAllUnenrolledHostIds, findUnenrolledHostByHostId, HostId } from './support/unenroll';
import { Agent, AgentStatus } from '../../../../../ingest_manager/common/types/models';
import { findAllUnenrolledAgentIds } from './support/unenroll';

interface HitSource {
_source: HostMetadata;
Expand Down Expand Up @@ -70,18 +70,20 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
},
async (context, req, res) => {
try {
const unenrolledHostIds = await findAllUnenrolledHostIds(
context.core.elasticsearch.legacy.client
const unenrolledAgentIds = await findAllUnenrolledAgentIds(
endpointAppContext.service.getAgentService(),
context.core.savedObjects.client
);

const queryParams = await kibanaRequestToMetadataListESQuery(
req,
endpointAppContext,
metadataIndexPattern,
{
unenrolledHostIds: unenrolledHostIds.map((host: HostId) => host.host.id),
unenrolledAgentIds,
}
);

const response = (await context.core.elasticsearch.legacy.client.callAsCurrentUser(
'search',
queryParams
Expand Down Expand Up @@ -138,13 +140,6 @@ export async function getHostData(
metadataRequestContext: MetadataRequestContext,
id: string
): Promise<HostInfo | undefined> {
const unenrolledHostId = await findUnenrolledHostByHostId(
metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client,
id
);
if (unenrolledHostId) {
throw Boom.badRequest('the requested endpoint is unenrolled');
}
const query = getESQueryHostMetadataByID(id, metadataIndexPattern);
const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client.callAsCurrentUser(
'search',
Expand All @@ -155,7 +150,36 @@ export async function getHostData(
return undefined;
}

return enrichHostMetadata(response.hits.hits[0]._source, metadataRequestContext);
const hostMetadata: HostMetadata = response.hits.hits[0]._source;
const agent = await findAgent(metadataRequestContext, hostMetadata);

if (agent && !agent.active) {
throw Boom.badRequest('the requested endpoint is unenrolled');
}

return enrichHostMetadata(hostMetadata, metadataRequestContext);
}

async function findAgent(
metadataRequestContext: MetadataRequestContext,
hostMetadata: HostMetadata
): Promise<Agent | undefined> {
const logger = metadataRequestContext.endpointAppContext.logFactory.get('metadata');
try {
return await metadataRequestContext.endpointAppContext.service
.getAgentService()
.getAgent(
metadataRequestContext.requestHandlerContext.core.savedObjects.client,
hostMetadata.elastic.agent.id
);
} catch (e) {
if (e.isBoom && e.output.statusCode === 404) {
logger.warn(`agent with id ${hostMetadata.elastic.agent.id} not found`);
return undefined;
} else {
throw e;
}
}
}

async function mapToHostResultList(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import Boom from 'boom';
import { EndpointAppContextService } from '../../endpoint_app_context_services';
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
import { HostId } from './support/unenroll';
import { Agent } from '../../../../../ingest_manager/common/types/models';

describe('test endpoint route', () => {
let routerMock: jest.Mocked<IRouter>;
Expand All @@ -51,12 +51,12 @@ describe('test endpoint route', () => {
typeof createMockEndpointAppContextServiceStartContract
>['agentService'];
let endpointAppContextService: EndpointAppContextService;
const noUnenrolledEndpoint = () =>
Promise.resolve(({
hits: {
hits: [],
},
} as unknown) as SearchResponse<HostId>);
const noUnenrolledAgent = {
agents: [],
total: 0,
page: 1,
perPage: 1,
};

beforeEach(() => {
mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked<
Expand Down Expand Up @@ -84,20 +84,19 @@ describe('test endpoint route', () => {
it('test find the latest of all endpoints', async () => {
const mockRequest = httpServerMock.createKibanaRequest({});
const response = createSearchResponse(new EndpointDocGenerator().generateHostMetadata());
mockScopedClient.callAsCurrentUser
.mockImplementationOnce(noUnenrolledEndpoint)
.mockImplementationOnce(() => Promise.resolve(response));
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;
mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent);
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
mockResponse
);

expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({ authRequired: true });
expect(mockResponse.ok).toBeCalled();
const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList;
Expand All @@ -122,11 +121,10 @@ describe('test endpoint route', () => {
});

mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockScopedClient.callAsCurrentUser
.mockImplementationOnce(noUnenrolledEndpoint)
.mockImplementationOnce(() =>
Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata()))
);
mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata()))
);
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;
Expand All @@ -137,8 +135,8 @@ describe('test endpoint route', () => {
mockResponse
);

expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2);
expect(mockScopedClient.callAsCurrentUser.mock.calls[1][1]?.body?.query).toEqual({
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({
match_all: {},
});
expect(routeConfig.options).toEqual({ authRequired: true });
Expand Down Expand Up @@ -167,11 +165,10 @@ describe('test endpoint route', () => {
});

mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockScopedClient.callAsCurrentUser
.mockImplementationOnce(noUnenrolledEndpoint)
.mockImplementationOnce(() =>
Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata()))
);
mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata()))
);
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;
Expand All @@ -183,7 +180,7 @@ describe('test endpoint route', () => {
);

expect(mockScopedClient.callAsCurrentUser).toBeCalled();
expect(mockScopedClient.callAsCurrentUser.mock.calls[1][1]?.body?.query).toEqual({
expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({
bool: {
must: [
{
Expand Down Expand Up @@ -218,11 +215,15 @@ describe('test endpoint route', () => {
it('should return 404 on no results', async () => {
const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } });

mockScopedClient.callAsCurrentUser
.mockImplementationOnce(noUnenrolledEndpoint)
.mockImplementationOnce(() => Promise.resolve(createSearchResponse()));
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve(createSearchResponse())
);

mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);

[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;
Expand All @@ -232,7 +233,7 @@ describe('test endpoint route', () => {
mockResponse
);

expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({ authRequired: true });
expect(mockResponse.notFound).toBeCalled();
const message = mockResponse.notFound.mock.calls[0][0]?.body;
Expand All @@ -246,9 +247,10 @@ describe('test endpoint route', () => {
});

mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online');
mockScopedClient.callAsCurrentUser
.mockImplementationOnce(noUnenrolledEndpoint)
.mockImplementationOnce(() => Promise.resolve(response));
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));

[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
Expand All @@ -260,7 +262,7 @@ describe('test endpoint route', () => {
mockResponse
);

expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({ authRequired: true });
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
Expand All @@ -279,9 +281,11 @@ describe('test endpoint route', () => {
throw Boom.notFound('Agent not found');
});

mockScopedClient.callAsCurrentUser
.mockImplementationOnce(noUnenrolledEndpoint)
.mockImplementationOnce(() => Promise.resolve(response));
mockAgentService.getAgent = 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')
Expand All @@ -293,7 +297,7 @@ describe('test endpoint route', () => {
mockResponse
);

expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(routeConfig.options).toEqual({ authRequired: true });
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
Expand All @@ -308,10 +312,10 @@ describe('test endpoint route', () => {
});

mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning');

mockScopedClient.callAsCurrentUser
.mockImplementationOnce(noUnenrolledEndpoint)
.mockImplementationOnce(() => Promise.resolve(response));
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: true,
} as unknown) as Agent);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));

[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
Expand All @@ -323,36 +327,23 @@ describe('test endpoint route', () => {
mockResponse
);

expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2);
expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
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 throw error when endpoint is unenrolled', async () => {
it('should throw error when endpoint egent is not active', async () => {
const response = createSearchResponse(new EndpointDocGenerator().generateHostMetadata());

const mockRequest = httpServerMock.createKibanaRequest({
params: { id: 'hostId' },
params: { id: response.hits.hits[0]._id },
});

mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve(({
hits: {
hits: [
{
_index: 'metrics-endpoint.metadata_mirror-default',
_id: 'S5M1yHIBLSMVtiLw6Wpr',
_score: 0.0,
_source: {
host: {
id: 'hostId',
},
},
},
],
},
} as unknown) as SearchResponse<HostId>)
);
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
mockAgentService.getAgent = jest.fn().mockReturnValue(({
active: false,
} as unknown) as Agent);

[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('query builder', () => {
'test default query params for all endpoints metadata when no params or body is provided ' +
'with unenrolled host ids excluded',
async () => {
const unenrolledHostId = '1fdca33f-799f-49f4-939c-ea4383c77672';
const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672';
const mockRequest = httpServerMock.createKibanaRequest({
body: {},
});
Expand All @@ -76,7 +76,7 @@ describe('query builder', () => {
},
metadataIndexPattern,
{
unenrolledHostIds: [unenrolledHostId],
unenrolledAgentIds: [unenrolledElasticAgentId],
}
);

Expand All @@ -86,7 +86,7 @@ describe('query builder', () => {
bool: {
must_not: {
terms: {
'host.id': ['1fdca33f-799f-49f4-939c-ea4383c77672'],
'elastic.agent.id': [unenrolledElasticAgentId],
},
},
},
Expand Down Expand Up @@ -198,7 +198,7 @@ describe('query builder', () => {
'test default query params for all endpoints endpoint metadata excluding unerolled endpoint ' +
'and when body filter is provided',
async () => {
const unenrolledHostId = '1fdca33f-799f-49f4-939c-ea4383c77672';
const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672';
const mockRequest = httpServerMock.createKibanaRequest({
body: {
filter: 'not host.ip:10.140.73.246',
Expand All @@ -213,7 +213,7 @@ describe('query builder', () => {
},
metadataIndexPattern,
{
unenrolledHostIds: [unenrolledHostId],
unenrolledAgentIds: [unenrolledElasticAgentId],
}
);

Expand All @@ -226,7 +226,7 @@ describe('query builder', () => {
bool: {
must_not: {
terms: {
'host.id': [unenrolledHostId],
'elastic.agent.id': [unenrolledElasticAgentId],
},
},
},
Expand Down
Loading

0 comments on commit eedb5f7

Please sign in to comment.