[Endpoint] Get current host info when retrieving alert details (#60906)

* create new alert details type

* update integration test

* add await to esarchiver call

* remove unused host stats type

* does the ui types good

* change host.host to host_metadata.host

* fix mock result type

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Davis Plumlee <davis.plumlee@elastic.co>
This commit is contained in:
marshallmain 2020-03-26 17:30:41 -04:00 committed by GitHub
parent 77a5b9e8ac
commit b1fa159e17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 73 additions and 37 deletions

View file

@ -239,11 +239,19 @@ interface AlertMetadata {
prev: string | null; prev: string | null;
} }
interface AlertState {
state: {
host_metadata: HostMetadata;
};
}
/** /**
* Union of alert data and metadata. * Union of alert data and metadata.
*/ */
export type AlertData = AlertEvent & AlertMetadata; export type AlertData = AlertEvent & AlertMetadata;
export type AlertDetails = AlertData & AlertState;
export type HostMetadata = Immutable<{ export type HostMetadata = Immutable<{
'@timestamp': number; '@timestamp': number;
event: { event: {

View file

@ -5,7 +5,7 @@
*/ */
import { IIndexPattern } from 'src/plugins/data/public'; import { IIndexPattern } from 'src/plugins/data/public';
import { Immutable, AlertData } from '../../../../../common/types'; import { Immutable, AlertDetails } from '../../../../../common/types';
import { AlertListData } from '../../types'; import { AlertListData } from '../../types';
interface ServerReturnedAlertsData { interface ServerReturnedAlertsData {
@ -15,7 +15,7 @@ interface ServerReturnedAlertsData {
interface ServerReturnedAlertDetailsData { interface ServerReturnedAlertDetailsData {
readonly type: 'serverReturnedAlertDetailsData'; readonly type: 'serverReturnedAlertDetailsData';
readonly payload: Immutable<AlertData>; readonly payload: Immutable<AlertDetails>;
} }
interface ServerReturnedSearchBarIndexPatterns { interface ServerReturnedSearchBarIndexPatterns {

View file

@ -5,7 +5,7 @@
*/ */
import { IIndexPattern } from 'src/plugins/data/public'; import { IIndexPattern } from 'src/plugins/data/public';
import { AlertResultList, AlertData } from '../../../../../common/types'; import { AlertResultList, AlertDetails } from '../../../../../common/types';
import { AppAction } from '../action'; import { AppAction } from '../action';
import { MiddlewareFactory, AlertListState } from '../../types'; import { MiddlewareFactory, AlertListState } from '../../types';
import { isOnAlertPage, apiQueryParams, hasSelectedAlert, uiQueryParams } from './selectors'; import { isOnAlertPage, apiQueryParams, hasSelectedAlert, uiQueryParams } from './selectors';
@ -40,7 +40,7 @@ export const alertMiddlewareFactory: MiddlewareFactory<AlertListState> = (coreSt
if (action.type === 'userChangedUrl' && isOnAlertPage(state) && hasSelectedAlert(state)) { if (action.type === 'userChangedUrl' && isOnAlertPage(state) && hasSelectedAlert(state)) {
const uiParams = uiQueryParams(state); const uiParams = uiQueryParams(state);
const response: AlertData = await coreStart.http.get( const response: AlertDetails = await coreStart.http.get(
`/api/endpoint/alerts/${uiParams.selected_alert}` `/api/endpoint/alerts/${uiParams.selected_alert}`
); );
api.dispatch({ type: 'serverReturnedAlertDetailsData', payload: response }); api.dispatch({ type: 'serverReturnedAlertDetailsData', payload: response });

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { AlertResultList } from '../../../../../common/types'; import { AlertResultList, AlertDetails } from '../../../../../common/types';
import { EndpointDocGenerator } from '../../../../../common/generate_data'; import { EndpointDocGenerator } from '../../../../../common/generate_data';
export const mockAlertResultList: (options?: { export const mockAlertResultList: (options?: {
@ -47,3 +47,18 @@ export const mockAlertResultList: (options?: {
}; };
return mock; return mock;
}; };
export const mockAlertDetailsResult = (): AlertDetails => {
const generator = new EndpointDocGenerator();
return {
...generator.generateAlert(new Date().getTime()),
...{
id: 'xDUYMHABAKk0XnHd8rrd',
prev: null,
next: null,
state: {
host_metadata: generator.generateHostMetadata(),
},
},
};
};

View file

@ -12,6 +12,7 @@ import {
AlertResultList, AlertResultList,
Immutable, Immutable,
ImmutableArray, ImmutableArray,
AlertDetails,
} from '../../../common/types'; } from '../../../common/types';
import { EndpointPluginStartDependencies } from '../../plugin'; import { EndpointPluginStartDependencies } from '../../plugin';
import { AppAction } from './store/action'; import { AppAction } from './store/action';
@ -196,7 +197,7 @@ export interface AlertListState {
readonly location?: Immutable<EndpointAppLocation>; readonly location?: Immutable<EndpointAppLocation>;
/** Specific Alert data to be shown in the details view */ /** Specific Alert data to be shown in the details view */
readonly alertDetails?: Immutable<AlertData>; readonly alertDetails?: Immutable<AlertDetails>;
/** Search bar state including indexPatterns */ /** Search bar state including indexPatterns */
readonly searchBar: AlertsSearchBarState; readonly searchBar: AlertsSearchBarState;

View file

@ -9,7 +9,7 @@ import { appStoreFactory } from '../../store';
import { fireEvent } from '@testing-library/react'; import { fireEvent } from '@testing-library/react';
import { MemoryHistory } from 'history'; import { MemoryHistory } from 'history';
import { AppAction } from '../../types'; import { AppAction } from '../../types';
import { mockAlertResultList } from '../../store/alerts/mock_alert_result_list'; import { mockAlertDetailsResult } from '../../store/alerts/mock_alert_result_list';
import { alertPageTestRender } from './test_helpers/render_alert_page'; import { alertPageTestRender } from './test_helpers/render_alert_page';
describe('when the alert details flyout is open', () => { describe('when the alert details flyout is open', () => {
@ -34,7 +34,7 @@ describe('when the alert details flyout is open', () => {
reactTestingLibrary.act(() => { reactTestingLibrary.act(() => {
const action: AppAction = { const action: AppAction = {
type: 'serverReturnedAlertDetailsData', type: 'serverReturnedAlertDetailsData',
payload: mockAlertResultList().alerts[0], payload: mockAlertDetailsResult(),
}; };
store.dispatch(action); store.dispatch(action);
}); });

View file

@ -9,6 +9,7 @@ import { AlertEvent, EndpointAppConstants } from '../../../../common/types';
import { EndpointAppContext } from '../../../types'; import { EndpointAppContext } from '../../../types';
import { AlertDetailsRequestParams } from '../types'; import { AlertDetailsRequestParams } from '../types';
import { AlertDetailsPagination } from './lib'; import { AlertDetailsPagination } from './lib';
import { getHostData } from '../../../routes/metadata';
export const alertDetailsHandlerWrapper = function( export const alertDetailsHandlerWrapper = function(
endpointAppContext: EndpointAppContext endpointAppContext: EndpointAppContext
@ -33,10 +34,15 @@ export const alertDetailsHandlerWrapper = function(
response response
); );
const currentHostInfo = await getHostData(ctx, response._source.host.id);
return res.ok({ return res.ok({
body: { body: {
id: response._id, id: response._id,
...response._source, ...response._source,
state: {
host_metadata: currentHostInfo,
},
next: await pagination.getNextUrl(), next: await pagination.getNextUrl(),
prev: await pagination.getPrevUrl(), prev: await pagination.getPrevUrl(),
}, },

View file

@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { IRouter } from 'kibana/server'; import { IRouter, RequestHandlerContext } from 'kibana/server';
import { SearchResponse } from 'elasticsearch'; import { SearchResponse } from 'elasticsearch';
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { import {
kibanaRequestToMetadataListESQuery, kibanaRequestToMetadataListESQuery,
kibanaRequestToMetadataGetESQuery, getESQueryHostMetadataByID,
} from '../services/endpoint/metadata_query_builders'; } from '../services/endpoint/metadata_query_builders';
import { HostMetadata, HostResultList } from '../../common/types'; import { HostMetadata, HostResultList } from '../../common/types';
import { EndpointAppContext } from '../types'; import { EndpointAppContext } from '../types';
@ -75,17 +75,11 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
}, },
async (context, req, res) => { async (context, req, res) => {
try { try {
const query = kibanaRequestToMetadataGetESQuery(req, endpointAppContext); const doc = await getHostData(context, req.params.id);
const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( if (doc) {
'search', return res.ok({ body: doc });
query
)) as SearchResponse<HostMetadata>;
if (response.hits.hits.length === 0) {
return res.notFound({ body: 'Endpoint Not Found' });
} }
return res.notFound({ body: 'Endpoint Not Found' });
return res.ok({ body: response.hits.hits[0]._source });
} catch (err) { } catch (err) {
return res.internalError({ body: err }); return res.internalError({ body: err });
} }
@ -93,6 +87,23 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
); );
} }
export async function getHostData(
context: RequestHandlerContext,
id: string
): Promise<HostMetadata | undefined> {
const query = getESQueryHostMetadataByID(id);
const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser(
'search',
query
)) as SearchResponse<HostMetadata>;
if (response.hits.hits.length === 0) {
return undefined;
}
return response.hits.hits[0]._source;
}
function mapToHostResultList( function mapToHostResultList(
queryParams: Record<string, any>, queryParams: Record<string, any>,
searchResponse: SearchResponse<HostMetadata> searchResponse: SearchResponse<HostMetadata>

View file

@ -7,7 +7,7 @@ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/s
import { EndpointConfigSchema } from '../../config'; import { EndpointConfigSchema } from '../../config';
import { import {
kibanaRequestToMetadataListESQuery, kibanaRequestToMetadataListESQuery,
kibanaRequestToMetadataGetESQuery, getESQueryHostMetadataByID,
} from './metadata_query_builders'; } from './metadata_query_builders';
import { EndpointAppConstants } from '../../../common/types'; import { EndpointAppConstants } from '../../../common/types';
@ -118,15 +118,7 @@ describe('query builder', () => {
describe('MetadataGetQuery', () => { describe('MetadataGetQuery', () => {
it('searches for the correct ID', () => { it('searches for the correct ID', () => {
const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899';
const mockRequest = httpServerMock.createKibanaRequest({ const query = getESQueryHostMetadataByID(mockID);
params: {
id: mockID,
},
});
const query = kibanaRequestToMetadataGetESQuery(mockRequest, {
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
});
expect(query).toEqual({ expect(query).toEqual({
body: { body: {
query: { match: { 'host.id.keyword': mockID } }, query: { match: { 'host.id.keyword': mockID } },

View file

@ -74,15 +74,12 @@ function buildQueryBody(request: KibanaRequest<any, any, any>): Record<string, a
}; };
} }
export const kibanaRequestToMetadataGetESQuery = ( export function getESQueryHostMetadataByID(hostID: string) {
request: KibanaRequest<any, any, any>,
endpointAppContext: EndpointAppContext
) => {
return { return {
body: { body: {
query: { query: {
match: { match: {
'host.id.keyword': request.params.id, 'host.id.keyword': hostID,
}, },
}, },
sort: [ sort: [
@ -96,4 +93,4 @@ export const kibanaRequestToMetadataGetESQuery = (
}, },
index: EndpointAppConstants.ENDPOINT_INDEX_NAME, index: EndpointAppConstants.ENDPOINT_INDEX_NAME,
}; };
}; }

View file

@ -72,13 +72,18 @@ export default function({ getService }: FtrProviderContext) {
describe('when data is in elasticsearch', () => { describe('when data is in elasticsearch', () => {
before(async () => { before(async () => {
await esArchiver.load('endpoint/alerts/api_feature'); await esArchiver.load('endpoint/alerts/api_feature');
await esArchiver.load('endpoint/metadata/api_feature');
const res = await es.search({ const res = await es.search({
index: 'events-endpoint-1', index: 'events-endpoint-1',
body: ES_QUERY_MISSING, body: ES_QUERY_MISSING,
}); });
nullableEventId = res.hits.hits[0]._source.event.id; nullableEventId = res.hits.hits[0]._source.event.id;
}); });
after(() => esArchiver.unload('endpoint/alerts/api_feature'));
after(async () => {
await esArchiver.unload('endpoint/alerts/api_feature');
await esArchiver.unload('endpoint/metadata/api_feature');
});
it('should not support POST requests', async () => { it('should not support POST requests', async () => {
await supertest await supertest
@ -381,6 +386,7 @@ export default function({ getService }: FtrProviderContext) {
expect(body.id).to.eql(documentID); expect(body.id).to.eql(documentID);
expect(body.prev).to.eql(`/api/endpoint/alerts/${prevDocumentID}`); expect(body.prev).to.eql(`/api/endpoint/alerts/${prevDocumentID}`);
expect(body.next).to.eql(null); // last alert, no more beyond this expect(body.next).to.eql(null); // last alert, no more beyond this
expect(body.state.host_metadata.host.id).to.eql(body.host.id);
}); });
it('should return alert details by id, getting first alert', async () => { it('should return alert details by id, getting first alert', async () => {