-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[RAM] Add alert summary API #146709
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RAM] Add alert summary API #146709
Changes from all commits
0bf35e3
54d2e06
0110c54
202cecb
c383de7
8f85cea
4cf0bc0
6760d3d
65a0536
fa414eb
12464f2
d35f5fa
df20fc2
ce4707c
eb96ff5
4a0cb36
f030944
c39342b
f892e9c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; | ||
| import { getAlertSummaryRoute } from './get_alert_summary'; | ||
| import { requestContextMock } from './__mocks__/request_context'; | ||
| import { requestMock, serverMock } from './__mocks__/server'; | ||
|
|
||
| describe('getAlertSummaryRoute', () => { | ||
| let server: ReturnType<typeof serverMock.create>; | ||
| let { clients, context } = requestContextMock.createTools(); | ||
|
|
||
| beforeEach(async () => { | ||
| server = serverMock.create(); | ||
| ({ clients, context } = requestContextMock.createTools()); | ||
|
|
||
| clients.rac.getAlertSummary.mockResolvedValue({ | ||
| activeAlertCount: 0, | ||
| recoveredAlertCount: 0, | ||
| activeAlerts: [], | ||
| recoveredAlerts: [], | ||
| }); | ||
|
|
||
| getAlertSummaryRoute(server.router); | ||
| }); | ||
|
|
||
| describe('request validation', () => { | ||
| test('rejects invalid query params', async () => { | ||
| await expect( | ||
| server.inject( | ||
| requestMock.create({ | ||
| method: 'post', | ||
| path: `${BASE_RAC_ALERTS_API_PATH}/_alert_summary`, | ||
| body: { gte: 4, lte: 3, featureIds: ['logs'] }, | ||
| }), | ||
| context | ||
| ) | ||
| ).rejects.toThrowErrorMatchingInlineSnapshot( | ||
| `"Request was rejected with message: 'Invalid value \\"4\\" supplied to \\"gte\\",Invalid value \\"3\\" supplied to \\"lte\\"'"` | ||
| ); | ||
| }); | ||
|
|
||
| test('validate gte/lte format', async () => { | ||
| const resp = await server.inject( | ||
| requestMock.create({ | ||
| method: 'post', | ||
| path: `${BASE_RAC_ALERTS_API_PATH}/_alert_summary`, | ||
| body: { | ||
| gte: '2020-12-16T15:00:00.000Z', | ||
| lte: '2020-12-16', | ||
| featureIds: ['logs'], | ||
| }, | ||
| }), | ||
| context | ||
| ); | ||
| expect(resp.status).toEqual(400); | ||
| expect(resp.body).toMatchInlineSnapshot(` | ||
| Object { | ||
| "attributes": Object { | ||
| "success": false, | ||
| }, | ||
| "message": "gte and/or lte are not following the UTC format", | ||
| } | ||
| `); | ||
| }); | ||
|
|
||
| test('validate fixed_interval ', async () => { | ||
| const resp = await server.inject( | ||
| requestMock.create({ | ||
| method: 'post', | ||
| path: `${BASE_RAC_ALERTS_API_PATH}/_alert_summary`, | ||
| body: { | ||
| gte: '2020-12-16T15:00:00.000Z', | ||
| lte: '2020-12-16T16:00:00.000Z', | ||
| featureIds: ['logs'], | ||
| fixed_interval: 'xx', | ||
| }, | ||
| }), | ||
| context | ||
| ); | ||
| expect(resp.status).toEqual(400); | ||
| expect(resp.body).toMatchInlineSnapshot(` | ||
| Object { | ||
| "attributes": Object { | ||
| "success": false, | ||
| }, | ||
| "message": "fixed_interval is not following the expected format 1m, 1h, 1d, 1w", | ||
| } | ||
| `); | ||
| }); | ||
|
|
||
| test('rejects unknown query params', async () => { | ||
| await expect( | ||
| server.inject( | ||
| requestMock.create({ | ||
| method: 'post', | ||
| path: `${BASE_RAC_ALERTS_API_PATH}/_alert_summary`, | ||
| body: { | ||
| gte: '2020-12-16T15:00:00.000Z', | ||
| lte: '2020-12-16T16:00:00.000Z', | ||
| featureIds: ['logs'], | ||
| boop: 'unknown', | ||
| }, | ||
| }), | ||
| context | ||
| ) | ||
| ).rejects.toThrowErrorMatchingInlineSnapshot( | ||
| `"Request was rejected with message: 'invalid keys \\"boop\\"'"` | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| import Boom from '@hapi/boom'; | ||
| import { IRouter } from '@kbn/core/server'; | ||
| import * as t from 'io-ts'; | ||
| import { transformError } from '@kbn/securitysolution-es-utils'; | ||
| import moment from 'moment'; | ||
| import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; | ||
|
|
||
| import { RacRequestHandlerContext } from '../types'; | ||
| import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; | ||
| import { buildRouteValidation } from './utils/route_validation'; | ||
|
|
||
| export const getAlertSummaryRoute = (router: IRouter<RacRequestHandlerContext>) => { | ||
| router.post( | ||
| { | ||
| path: `${BASE_RAC_ALERTS_API_PATH}/_alert_summary`, | ||
| validate: { | ||
| body: buildRouteValidation( | ||
| t.intersection([ | ||
| t.exact( | ||
| t.type({ | ||
| gte: t.string, | ||
| lte: t.string, | ||
| featureIds: t.array(t.string), | ||
| }) | ||
| ), | ||
| t.exact( | ||
| t.partial({ | ||
| fixed_interval: t.string, | ||
| filter: t.array(t.object), | ||
| }) | ||
| ), | ||
| ]) | ||
| ), | ||
| }, | ||
| options: { | ||
| tags: ['access:rac'], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this option do?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is for kibana security, I think we created a special |
||
| }, | ||
| }, | ||
| async (context, request, response) => { | ||
| try { | ||
| const racContext = await context.rac; | ||
| const alertsClient = await racContext.getAlertsClient(); | ||
| const { gte, lte, featureIds, filter, fixed_interval: fixedInterval } = request.body; | ||
| if ( | ||
| !( | ||
| moment(gte, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true).isValid() && | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use DateFromString instead to validate it at the body validation level?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I only imagined that will use this UTC format and not just a date. However, if you feel that we should allow just date without time, I can change it back to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it is fine, I also used the same time format in my PR for the related component. |
||
| moment(lte, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true).isValid() | ||
| ) | ||
| ) { | ||
| throw Boom.badRequest('gte and/or lte are not following the UTC format'); | ||
| } | ||
|
|
||
| if (fixedInterval && fixedInterval?.match(/^\d{1,2}['m','h','d','w']$/) == null) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we validate this with a custom validator on the body params?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did not find an easy way to do it with io-ts, so I will keep it this way for now. |
||
| throw Boom.badRequest( | ||
| 'fixed_interval is not following the expected format 1m, 1h, 1d, 1w' | ||
| ); | ||
| } | ||
|
|
||
| const aggs = await alertsClient.getAlertSummary({ | ||
| gte, | ||
| lte, | ||
| featureIds, | ||
| filter: filter as estypes.QueryDslQueryContainer[], | ||
| fixedInterval, | ||
| }); | ||
| return response.ok({ | ||
| body: aggs, | ||
| }); | ||
| } catch (exc) { | ||
| const err = transformError(exc); | ||
| const contentType = { | ||
| 'content-type': 'application/json', | ||
| }; | ||
| const defaultedHeaders = { | ||
| ...contentType, | ||
| }; | ||
| return response.customError({ | ||
| headers: defaultedHeaders, | ||
| statusCode: err.statusCode, | ||
| body: { | ||
| message: err.message, | ||
| attributes: { | ||
| success: false, | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
| } | ||
| ); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.