diff --git a/x-pack/plugins/endpoint/common/alert_constants.ts b/x-pack/plugins/endpoint/common/alert_constants.ts index 85e1643d684f2..66de2b85ef3a7 100644 --- a/x-pack/plugins/endpoint/common/alert_constants.ts +++ b/x-pack/plugins/endpoint/common/alert_constants.ts @@ -13,10 +13,6 @@ export class AlertConstants { * The path for the Alert's Index Pattern API. */ static INDEX_PATTERN_ROUTE = `${AlertConstants.BASE_API_URL}/index_pattern`; - /** - * Alert's Index pattern - */ - static ALERT_INDEX_NAME = 'events-endpoint-1'; /** * A paramter passed to Alert's Index Pattern. */ diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts index 92f8aacbf26a2..ab6d1850e425d 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts @@ -6,11 +6,11 @@ import { GetResponse } from 'elasticsearch'; import { KibanaRequest, RequestHandler } from 'kibana/server'; import { AlertEvent } from '../../../../common/types'; -import { AlertConstants } from '../../../../common/alert_constants'; import { EndpointAppContext } from '../../../types'; import { AlertDetailsRequestParams } from '../types'; import { AlertDetailsPagination } from './lib'; import { getHostData } from '../../metadata'; +import { AlertId, AlertIdError } from '../lib'; export const alertDetailsHandlerWrapper = function( endpointAppContext: EndpointAppContext @@ -21,10 +21,10 @@ export const alertDetailsHandlerWrapper = function( res ) => { try { - const alertId = req.params.id; + const alertId = AlertId.fromEncoded(req.params.id); const response = (await ctx.core.elasticsearch.dataClient.callAsCurrentUser('get', { - index: AlertConstants.ALERT_INDEX_NAME, - id: alertId, + index: alertId.index, + id: alertId.id, })) as GetResponse; const indexPattern = await endpointAppContext.service @@ -50,7 +50,7 @@ export const alertDetailsHandlerWrapper = function( return res.ok({ body: { - id: response._id, + id: alertId.toString(), ...response._source, state: { host_metadata: currentHostInfo?.metadata, @@ -60,7 +60,13 @@ export const alertDetailsHandlerWrapper = function( }, }); } catch (err) { - if (err.status === 404) { + const logger = endpointAppContext.logFactory.get('alerts'); + logger.warn(err); + + // err will be an AlertIdError if the passed in alert id is not valid + if (err instanceof AlertIdError) { + return res.badRequest({ body: err }); + } else if (err.status === 404) { return res.notFound({ body: err }); } return res.internalError({ body: err }); diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts index 0f69e1bb60c44..5c95b1217d829 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts @@ -8,7 +8,7 @@ import { GetResponse, SearchResponse } from 'elasticsearch'; import { AlertEvent, AlertHits, AlertAPIOrdering } from '../../../../../common/types'; import { AlertConstants } from '../../../../../common/alert_constants'; import { EndpointConfigType } from '../../../../config'; -import { searchESForAlerts, Pagination } from '../../lib'; +import { searchESForAlerts, Pagination, AlertId } from '../../lib'; import { AlertSearchQuery, SearchCursor, AlertDetailsRequestParams } from '../../types'; import { BASE_ALERTS_ROUTE } from '../..'; import { RequestHandlerContext } from '../../../../../../../../src/core/server'; @@ -59,7 +59,8 @@ export class AlertDetailsPagination extends Pagination< protected getUrlFromHits(hits: AlertHits): string | null { if (hits.length > 0) { - return `${BASE_ALERTS_ROUTE}/${hits[0]._id}`; + const id = new AlertId(hits[0]._index, hits[0]._id); + return `${BASE_ALERTS_ROUTE}/${id.toString()}`; } return null; } diff --git a/x-pack/plugins/endpoint/server/routes/alerts/lib/alert_id.ts b/x-pack/plugins/endpoint/server/routes/alerts/lib/alert_id.ts new file mode 100644 index 0000000000000..797bf69f5991a --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/alerts/lib/alert_id.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertIdError } from './error'; + +/** + * Abstraction over alert IDs. + */ +export class AlertId { + protected readonly _index: string; + protected readonly _id: string; + + constructor(index: string, id: string) { + this._index = index; + this._id = id; + } + + public get index() { + return this._index; + } + + public get id() { + return this._id; + } + + static fromEncoded(encoded: string): AlertId { + try { + const value = encoded.replace(/\-/g, '+').replace(/_/g, '/'); + const data = Buffer.from(value, 'base64').toString('utf8'); + const { index, id } = JSON.parse(data); + return new AlertId(index, id); + } catch (error) { + throw new AlertIdError(`Unable to decode alert id: ${encoded}`); + } + } + + toString(): string { + const value = JSON.stringify({ index: this.index, id: this.id }); + // replace invalid URL characters with valid ones + return Buffer.from(value, 'utf8') + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); + } +} diff --git a/x-pack/plugins/endpoint/server/routes/alerts/lib/error.ts b/x-pack/plugins/endpoint/server/routes/alerts/lib/error.ts new file mode 100644 index 0000000000000..7b00634b25c9c --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/alerts/lib/error.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class AlertIdError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts index 7bc1c0c306ae2..1c98e3c615669 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts @@ -17,7 +17,9 @@ import { UndefinedResultPosition, } from '../types'; +export { AlertIdError } from './error'; export { Pagination } from './pagination'; +export { AlertId } from './alert_id'; function reverseSortDirection(order: AlertAPIOrdering): AlertAPIOrdering { if (order === 'asc') { diff --git a/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts index 114251820ce4b..0af8f6cf792dd 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts @@ -20,6 +20,7 @@ import { AlertConstants } from '../../../../../common/alert_constants'; import { EndpointAppContext } from '../../../../types'; import { AlertSearchQuery } from '../../types'; import { AlertListPagination } from './pagination'; +import { AlertId } from '../../lib'; export const getRequestData = async ( request: KibanaRequest, @@ -105,8 +106,9 @@ export async function mapToAlertResultList( const pagination: AlertListPagination = new AlertListPagination(config, reqCtx, reqData, hits); function mapHit(entry: AlertHits[0]): AlertData { + const alertId = new AlertId(entry._index, entry._id); return { - id: entry._id, + id: alertId.toString(), ...entry._source, prev: null, next: null, diff --git a/x-pack/test/api_integration/apis/endpoint/alerts.ts b/x-pack/test/api_integration/apis/endpoint/alerts.ts index 7e72a2f9072f3..94aaab2530247 100644 --- a/x-pack/test/api_integration/apis/endpoint/alerts.ts +++ b/x-pack/test/api_integration/apis/endpoint/alerts.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; import { AlertData } from '../../../../plugins/endpoint/common/types'; +import { AlertId } from '../../../../plugins/endpoint/server/routes/alerts/lib/index'; /** * The number of alert documents in the es archive. @@ -65,6 +66,7 @@ export default function({ getService }: FtrProviderContext) { const nextPrevPrefixOrder = 'order=desc'; const nextPrevPrefixPageSize = 'page_size=10'; const nextPrevPrefix = `${nextPrevPrefixQuery}&${nextPrevPrefixDateRange}&${nextPrevPrefixSort}&${nextPrevPrefixOrder}&${nextPrevPrefixPageSize}`; + const alertIndex = 'events-endpoint-1'; let nullableEventId = ''; @@ -74,7 +76,7 @@ export default function({ getService }: FtrProviderContext) { await esArchiver.load('endpoint/alerts/api_feature'); await esArchiver.load('endpoint/alerts/host_api_feature'); const res = await es.search({ - index: 'events-endpoint-1', + index: alertIndex, body: ES_QUERY_MISSING, }); nullableEventId = res.hits.hits[0]._source.event.id; @@ -377,36 +379,45 @@ export default function({ getService }: FtrProviderContext) { }); it('should return alert details by id, getting last alert', async () => { - const documentID = 'zbNm0HABdD75WLjLYgcB'; - const prevDocumentID = '2rNm0HABdD75WLjLYgcU'; + const documentID = new AlertId(alertIndex, 'zbNm0HABdD75WLjLYgcB'); + const prevDocumentID = new AlertId(alertIndex, '2rNm0HABdD75WLjLYgcU'); const { body } = await supertest - .get(`/api/endpoint/alerts/${documentID}`) + .get(`/api/endpoint/alerts/${documentID.toString()}`) .set('kbn-xsrf', 'xxx') .expect(200); - expect(body.id).to.eql(documentID); - expect(body.prev).to.eql(`/api/endpoint/alerts/${prevDocumentID}`); + expect(body.id).to.eql(documentID.toString()); + expect(body.prev).to.eql(`/api/endpoint/alerts/${prevDocumentID.toString()}`); 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 () => { - const documentID = 'p7Nm0HABdD75WLjLYghv'; - const nextDocumentID = 'mbNm0HABdD75WLjLYgho'; + const documentID = new AlertId(alertIndex, 'p7Nm0HABdD75WLjLYghv'); + const nextDocumentID = new AlertId(alertIndex, 'mbNm0HABdD75WLjLYgho'); const { body } = await supertest - .get(`/api/endpoint/alerts/${documentID}`) + .get(`/api/endpoint/alerts/${documentID.toString()}`) .set('kbn-xsrf', 'xxx') .expect(200); - expect(body.id).to.eql(documentID); - expect(body.next).to.eql(`/api/endpoint/alerts/${nextDocumentID}`); + expect(body.id).to.eql(documentID.toString()); + expect(body.next).to.eql(`/api/endpoint/alerts/${nextDocumentID.toString()}`); expect(body.prev).to.eql(null); // first alert, no more before this }); it('should return 404 when alert is not found', async () => { + const documentID = new AlertId(alertIndex, 'does-not-exit'); + await supertest - .get('/api/endpoint/alerts/does-not-exist') + .get(`/api/endpoint/alerts/${documentID.toString()}`) .set('kbn-xsrf', 'xxx') .expect(404); }); + + it('should return 400 when alert id is not valid', async () => { + await supertest + .get('/api/endpoint/alerts/does-not-exist') + .set('kbn-xsrf', 'xxx') + .expect(400); + }); }); }); }