[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;
}
interface AlertState {
state: {
host_metadata: HostMetadata;
};
}
/**
* Union of alert data and metadata.
*/
export type AlertData = AlertEvent & AlertMetadata;
export type AlertDetails = AlertData & AlertState;
export type HostMetadata = Immutable<{
'@timestamp': number;
event: {

View file

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

View file

@ -5,7 +5,7 @@
*/
import { IIndexPattern } from 'src/plugins/data/public';
import { AlertResultList, AlertData } from '../../../../../common/types';
import { AlertResultList, AlertDetails } from '../../../../../common/types';
import { AppAction } from '../action';
import { MiddlewareFactory, AlertListState } from '../../types';
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)) {
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.dispatch({ type: 'serverReturnedAlertDetailsData', payload: response });

View file

@ -4,7 +4,7 @@
* 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';
export const mockAlertResultList: (options?: {
@ -47,3 +47,18 @@ export const mockAlertResultList: (options?: {
};
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,
Immutable,
ImmutableArray,
AlertDetails,
} from '../../../common/types';
import { EndpointPluginStartDependencies } from '../../plugin';
import { AppAction } from './store/action';
@ -196,7 +197,7 @@ export interface AlertListState {
readonly location?: Immutable<EndpointAppLocation>;
/** Specific Alert data to be shown in the details view */
readonly alertDetails?: Immutable<AlertData>;
readonly alertDetails?: Immutable<AlertDetails>;
/** Search bar state including indexPatterns */
readonly searchBar: AlertsSearchBarState;

View file

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

View file

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

View file

@ -4,13 +4,13 @@
* 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 { schema } from '@kbn/config-schema';
import {
kibanaRequestToMetadataListESQuery,
kibanaRequestToMetadataGetESQuery,
getESQueryHostMetadataByID,
} from '../services/endpoint/metadata_query_builders';
import { HostMetadata, HostResultList } from '../../common/types';
import { EndpointAppContext } from '../types';
@ -75,17 +75,11 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
},
async (context, req, res) => {
try {
const query = kibanaRequestToMetadataGetESQuery(req, endpointAppContext);
const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser(
'search',
query
)) as SearchResponse<HostMetadata>;
if (response.hits.hits.length === 0) {
return res.notFound({ body: 'Endpoint Not Found' });
const doc = await getHostData(context, req.params.id);
if (doc) {
return res.ok({ body: doc });
}
return res.ok({ body: response.hits.hits[0]._source });
return res.notFound({ body: 'Endpoint Not Found' });
} catch (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(
queryParams: Record<string, any>,
searchResponse: SearchResponse<HostMetadata>

View file

@ -7,7 +7,7 @@ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/s
import { EndpointConfigSchema } from '../../config';
import {
kibanaRequestToMetadataListESQuery,
kibanaRequestToMetadataGetESQuery,
getESQueryHostMetadataByID,
} from './metadata_query_builders';
import { EndpointAppConstants } from '../../../common/types';
@ -118,15 +118,7 @@ describe('query builder', () => {
describe('MetadataGetQuery', () => {
it('searches for the correct ID', () => {
const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899';
const mockRequest = httpServerMock.createKibanaRequest({
params: {
id: mockID,
},
});
const query = kibanaRequestToMetadataGetESQuery(mockRequest, {
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
});
const query = getESQueryHostMetadataByID(mockID);
expect(query).toEqual({
body: {
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 = (
request: KibanaRequest<any, any, any>,
endpointAppContext: EndpointAppContext
) => {
export function getESQueryHostMetadataByID(hostID: string) {
return {
body: {
query: {
match: {
'host.id.keyword': request.params.id,
'host.id.keyword': hostID,
},
},
sort: [
@ -96,4 +93,4 @@ export const kibanaRequestToMetadataGetESQuery = (
},
index: EndpointAppConstants.ENDPOINT_INDEX_NAME,
};
};
}

View file

@ -72,13 +72,18 @@ export default function({ getService }: FtrProviderContext) {
describe('when data is in elasticsearch', () => {
before(async () => {
await esArchiver.load('endpoint/alerts/api_feature');
await esArchiver.load('endpoint/metadata/api_feature');
const res = await es.search({
index: 'events-endpoint-1',
body: ES_QUERY_MISSING,
});
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 () => {
await supertest
@ -381,6 +386,7 @@ export default function({ getService }: FtrProviderContext) {
expect(body.id).to.eql(documentID);
expect(body.prev).to.eql(`/api/endpoint/alerts/${prevDocumentID}`);
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 () => {