diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts index 59db45aff0fa2..e636af632464c 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.mock.ts @@ -20,6 +20,7 @@ const createAlertsClientMock = () => { find: jest.fn(), getFeatureIdsByRegistrationContexts: jest.fn(), getBrowserFields: jest.fn(), + getAlertSummary: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index 964d19e45b613..9aa0bbd7343cb 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -11,11 +11,16 @@ import { Filter, buildEsQuery, EsQueryConfig } from '@kbn/es-query'; import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils'; import { AlertConsumers, + ALERT_TIME_RANGE, + ALERT_STATUS, getEsQueryConfig, getSafeSortIds, isValidFeatureId, STATUS_VALUES, ValidFeatureId, + ALERT_STATUS_RECOVERED, + ALERT_END, + ALERT_STATUS_ACTIVE, } from '@kbn/rule-data-utils'; import { @@ -32,6 +37,7 @@ import { import { Logger, ElasticsearchClient, EcsEventOutcome } from '@kbn/core/server'; import { AuditLogger } from '@kbn/security-plugin/server'; import { IndexPatternsFetcher } from '@kbn/data-plugin/server'; +import { isEmpty } from 'lodash'; import { BrowserFields } from '../../common'; import { alertAuditEvent, operationAlertAuditActionMap } from './audit_events'; import { @@ -92,6 +98,15 @@ interface GetAlertParams { index?: string; } +interface GetAlertSummaryParams { + id?: string; + gte: string; + lte: string; + featureIds: string[]; + filter?: estypes.QueryDslQueryContainer[]; + fixedInterval?: string; +} + interface SingleSearchAfterAndAudit { id?: string | null | undefined; query?: string | object | undefined; @@ -500,6 +515,115 @@ export class AlertsClient { } } + public async getAlertSummary({ + gte, + lte, + featureIds, + filter, + fixedInterval = '1m', + }: GetAlertSummaryParams) { + try { + const indexToUse = await this.getAuthorizedAlertsIndices(featureIds); + + if (isEmpty(indexToUse)) { + throw Boom.badRequest('no featureIds were provided for getting alert summary'); + } + + // first search for the alert by id, then use the alert info to check if user has access to it + const responseAlertSum = await this.singleSearchAfterAndAudit({ + index: (indexToUse ?? []).join(), + operation: ReadOperations.Get, + aggs: { + active_alerts_bucket: { + date_histogram: { + field: ALERT_TIME_RANGE, + fixed_interval: fixedInterval, + hard_bounds: { + min: gte, + max: lte, + }, + extended_bounds: { + min: gte, + max: lte, + }, + min_doc_count: 0, + }, + }, + recovered_alerts: { + filter: { + term: { + [ALERT_STATUS]: ALERT_STATUS_RECOVERED, + }, + }, + aggs: { + container: { + date_histogram: { + field: ALERT_END, + fixed_interval: fixedInterval, + extended_bounds: { + min: gte, + max: lte, + }, + min_doc_count: 0, + }, + }, + }, + }, + count: { + terms: { field: ALERT_STATUS }, + }, + }, + query: { + bool: { + filter: [ + { + range: { + [ALERT_TIME_RANGE]: { + gt: gte, + lt: lte, + }, + }, + }, + ...(filter ? filter : []), + ], + }, + }, + size: 0, + }); + + let activeAlertCount = 0; + let recoveredAlertCount = 0; + ( + (responseAlertSum.aggregations?.count as estypes.AggregationsMultiBucketAggregateBase) + .buckets as estypes.AggregationsStringTermsBucketKeys[] + ).forEach((b) => { + if (b.key === ALERT_STATUS_ACTIVE) { + activeAlertCount = b.doc_count; + } else if (b.key === ALERT_STATUS_RECOVERED) { + recoveredAlertCount = b.doc_count; + } + }); + + return { + activeAlertCount, + recoveredAlertCount, + activeAlerts: + ( + responseAlertSum.aggregations + ?.active_alerts_bucket as estypes.AggregationsAutoDateHistogramAggregate + )?.buckets ?? [], + recoveredAlerts: + ( + (responseAlertSum.aggregations?.recovered_alerts as estypes.AggregationsFilterAggregate) + ?.container as estypes.AggregationsAutoDateHistogramAggregate + )?.buckets ?? [], + }; + } catch (error) { + this.logger.error(`getAlertSummary threw an error: ${error}`); + throw error; + } + } + public async update({ id, status, diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_summary.test.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_summary.test.ts new file mode 100644 index 0000000000000..3496b8f342e2c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_summary.test.ts @@ -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; + 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\\"'"` + ); + }); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_summary.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_summary.ts new file mode 100644 index 0000000000000..6125c2a39b845 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_summary.ts @@ -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) => { + 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'], + }, + }, + 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() && + 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) { + 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, + }, + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/rule_registry/server/routes/index.ts b/x-pack/plugins/rule_registry/server/routes/index.ts index a693de9b2fa4c..7a71f19f7980f 100644 --- a/x-pack/plugins/rule_registry/server/routes/index.ts +++ b/x-pack/plugins/rule_registry/server/routes/index.ts @@ -14,6 +14,7 @@ import { bulkUpdateAlertsRoute } from './bulk_update_alerts'; import { findAlertsByQueryRoute } from './find'; import { getFeatureIdsByRegistrationContexts } from './get_feature_ids_by_registration_contexts'; import { getBrowserFieldsByFeatureId } from './get_browser_fields_by_feature_id'; +import { getAlertSummaryRoute } from './get_alert_summary'; export function defineRoutes(router: IRouter) { getAlertByIdRoute(router); @@ -23,4 +24,5 @@ export function defineRoutes(router: IRouter) { findAlertsByQueryRoute(router); getFeatureIdsByRegistrationContexts(router); getBrowserFieldsByFeatureId(router); + getAlertSummaryRoute(router); } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index c901cca33f450..cc7c9c9e6f402 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -130,6 +130,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAlertSummary", ] `); }); @@ -179,6 +180,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAlertSummary", ] `); }); @@ -276,6 +278,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", ] `); @@ -342,6 +345,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", ] `); @@ -446,10 +450,12 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/getAuthorizedAlertsIndices", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/getAlertSummary", ] `); }); @@ -521,10 +527,12 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/getAuthorizedAlertsIndices", + "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/getAlertSummary", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/update", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/getAuthorizedAlertsIndices", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/getAlertSummary", ] `); }); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index 94b8c1a64290c..647464c714325 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -25,7 +25,7 @@ const readOperations: Record = { 'find', 'getRuleExecutionKPI', ], - alert: ['get', 'find', 'getAuthorizedAlertsIndices'], + alert: ['get', 'find', 'getAuthorizedAlertsIndices', 'getAlertSummary'], }; const writeOperations: Record = { diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json index 65719c78a864e..ce709e126266f 100644 --- a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json @@ -10,6 +10,10 @@ "message": "hello world 1", "kibana.alert.rule.consumer": "apm", "kibana.alert.workflow_status": "open", + "kibana.alert.time_range": { + "gte": "2020-12-16T15:16:18.570Z" + }, + "kibana.alert.status": "active", "kibana.space_ids": ["space1", "space2"] } } @@ -26,7 +30,12 @@ "kibana.alert.rule.rule_type_id": "apm.error_rate", "message": "hello world 1", "kibana.alert.rule.consumer": "apm", - "kibana.alert.workflow_status": "open", + "kibana.alert.workflow_status": "recovered", + "kibana.alert.time_range": { + "gte": "2020-12-16T15:16:18.570Z", + "lte": "2020-12-16T15:16:19.570Z" + }, + "kibana.alert.status": "recovered", "kibana.space_ids": ["space1"] } } @@ -44,6 +53,7 @@ "message": "hello world 1", "kibana.alert.rule.consumer": "apm", "kibana.alert.workflow_status": "open", + "kibana.alert.status": "active", "kibana.space_ids": ["space2"] } } @@ -114,3 +124,47 @@ } } } + +{ + "type": "doc", + "value": { + "index": ".alerts-observability.logs.alerts-default", + "id": "123456789XYZ", + "source": { + "event.kind": "signal", + "@timestamp": "2020-12-16T15:16:18.570Z", + "kibana.alert.rule.rule_type_id": "apm.error_rate", + "message": "hello world 1", + "kibana.alert.rule.consumer": "apm", + "kibana.alert.workflow_status": "open", + "kibana.alert.time_range": { + "gte": "2020-12-16T15:16:18.570Z" + }, + "kibana.alert.status": "active", + "kibana.space_ids": ["space1", "space2"] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".alerts-observability.logs.alerts-default", + "id": "space1alertLogs", + "source": { + "event.kind": "signal", + "@timestamp": "2020-12-16T15:16:18.570Z", + "kibana.alert.rule.rule_type_id": "apm.error_rate", + "message": "hello world 1", + "kibana.alert.rule.consumer": "apm", + "kibana.alert.workflow_status": "recovered", + "kibana.alert.time_range": { + "gte": "2020-12-16T15:16:18.570Z", + "lte": "2020-12-16T15:27:19.570Z" + }, + "kibana.alert.end": "2020-12-16T15:27:19.570Z", + "kibana.alert.status": "recovered", + "kibana.space_ids": ["space1"] + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json index f55241f25c90e..16229e93e6255 100644 --- a/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json @@ -16,6 +16,10 @@ "kibana.alert.rule.consumer": { "type": "keyword", "ignore_above": 256 + }, + "kibana.alert.time_range": { + "type": "date_range", + "format": "epoch_millis||strict_date_optional_time" } } } @@ -45,3 +49,38 @@ } } } + +{ + "type": "index", + "value": { + "index": ".alerts-observability.logs.alerts-default", + "mappings": { + "properties": { + "message": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "kibana.alert.rule.consumer": { + "type": "keyword", + "ignore_above": 256 + }, + "kibana.alert.status": { + "type": "keyword", + "ignore_above": 256 + }, + "kibana.alert.time_range": { + "type": "date_range", + "format": "epoch_millis||strict_date_optional_time" + }, + "kibana.alert.end": { + "type": "date" + } + } + } + } +} diff --git a/x-pack/test/rule_registry/common/lib/authentication/roles.ts b/x-pack/test/rule_registry/common/lib/authentication/roles.ts index 5a75f5bc4f8da..36fe754d3e50a 100644 --- a/x-pack/test/rule_registry/common/lib/authentication/roles.ts +++ b/x-pack/test/rule_registry/common/lib/authentication/roles.ts @@ -109,6 +109,7 @@ export const observabilityOnlyAll: Role = { { feature: { apm: ['all'], + logs: ['all'], }, spaces: ['space1'], }, @@ -126,6 +127,7 @@ export const observabilityOnlyAllSpace2: Role = { { feature: { apm: ['all'], + logs: ['all'], }, spaces: ['space2'], }, @@ -143,6 +145,7 @@ export const observabilityOnlyRead: Role = { { feature: { apm: ['read'], + logs: ['read'], }, spaces: ['space1'], }, @@ -160,6 +163,7 @@ export const observabilityOnlyReadSpace2: Role = { { feature: { apm: ['read'], + logs: ['read'], }, spaces: ['space2'], }, @@ -214,6 +218,7 @@ export const observabilityOnlyAllSpacesAll: Role = { { feature: { apm: ['all'], + logs: ['all'], }, spaces: ['*'], }, @@ -260,6 +265,7 @@ export const observabilityOnlyAllSpacesAllWithReadESIndices: Role = { { feature: { apm: ['all'], + logs: ['all'], }, spaces: ['default', 'space1'], }, @@ -277,6 +283,7 @@ export const observabilityOnlyReadSpacesAll: Role = { { feature: { apm: ['read'], + logs: ['read'], }, spaces: ['*'], }, diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_summary.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_summary.ts new file mode 100644 index 0000000000000..b22fc830cb73d --- /dev/null +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alert_summary.ts @@ -0,0 +1,159 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces'; +import { superUser } from '../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const TEST_URL = '/internal/rac/alerts'; + const SPACE1 = 'space1'; + const ALERT_SUMMARY_URL = `${TEST_URL}/_alert_summary`; + const LOGS_ALERT_ID = '123456789XYZ'; + const LOGS_ALERT_ID2 = 'space1alertLogs'; + + describe('Alerts - GET - _alert_summary', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); + }); + + it('Alert summary for all LOGS alerts with features', async () => { + const alertSummary = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${ALERT_SUMMARY_URL}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + gte: '2020-12-16T15:00:00.000Z', + lte: '2020-12-16T16:00:00.000Z', + featureIds: ['logs'], + fixed_interval: '10m', + }) + .expect(200); + + expect(alertSummary.body).to.eql({ + activeAlertCount: 1, + recoveredAlertCount: 1, + activeAlerts: [ + { key_as_string: '1608130800000', key: 1608130800000, doc_count: 0 }, + { key_as_string: '1608131400000', key: 1608131400000, doc_count: 2 }, + { key_as_string: '1608132000000', key: 1608132000000, doc_count: 2 }, + { key_as_string: '1608132600000', key: 1608132600000, doc_count: 1 }, + { key_as_string: '1608133200000', key: 1608133200000, doc_count: 1 }, + { key_as_string: '1608133800000', key: 1608133800000, doc_count: 1 }, + { key_as_string: '1608134400000', key: 1608134400000, doc_count: 1 }, + ], + recoveredAlerts: [ + { key_as_string: '2020-12-16T15:00:00.000Z', key: 1608130800000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:10:00.000Z', key: 1608131400000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:20:00.000Z', key: 1608132000000, doc_count: 1 }, + { key_as_string: '2020-12-16T15:30:00.000Z', key: 1608132600000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:40:00.000Z', key: 1608133200000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:50:00.000Z', key: 1608133800000, doc_count: 0 }, + { key_as_string: '2020-12-16T16:00:00.000Z', key: 1608134400000, doc_count: 0 }, + ], + }); + }); + + it('Alert summary with a filter where the alert is recovered', async () => { + const alertSummary = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${ALERT_SUMMARY_URL}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + gte: '2020-12-16T15:00:00.000Z', + lte: '2020-12-16T16:00:00.000Z', + filter: [ + { + terms: { + _id: [LOGS_ALERT_ID2], + }, + }, + ], + featureIds: ['logs'], + fixed_interval: '10m', + }) + .expect(200); + + expect(alertSummary.body).to.eql({ + activeAlertCount: 0, + recoveredAlertCount: 1, + activeAlerts: [ + { key_as_string: '1608130800000', key: 1608130800000, doc_count: 0 }, + { key_as_string: '1608131400000', key: 1608131400000, doc_count: 1 }, + { key_as_string: '1608132000000', key: 1608132000000, doc_count: 1 }, + { key_as_string: '1608132600000', key: 1608132600000, doc_count: 0 }, + { key_as_string: '1608133200000', key: 1608133200000, doc_count: 0 }, + { key_as_string: '1608133800000', key: 1608133800000, doc_count: 0 }, + { key_as_string: '1608134400000', key: 1608134400000, doc_count: 0 }, + ], + recoveredAlerts: [ + { key_as_string: '2020-12-16T15:00:00.000Z', key: 1608130800000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:10:00.000Z', key: 1608131400000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:20:00.000Z', key: 1608132000000, doc_count: 1 }, + { key_as_string: '2020-12-16T15:30:00.000Z', key: 1608132600000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:40:00.000Z', key: 1608133200000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:50:00.000Z', key: 1608133800000, doc_count: 0 }, + { key_as_string: '2020-12-16T16:00:00.000Z', key: 1608134400000, doc_count: 0 }, + ], + }); + }); + + it('Alert summary with a filter where the alert is active', async () => { + const alertSummary = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${ALERT_SUMMARY_URL}`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + gte: '2020-12-16T15:00:00.000Z', + lte: '2020-12-16T16:00:00.000Z', + filter: [ + { + terms: { + _id: [LOGS_ALERT_ID], + }, + }, + ], + featureIds: ['logs'], + fixed_interval: '10m', + }) + .expect(200); + + expect(alertSummary.body).to.eql({ + activeAlertCount: 1, + recoveredAlertCount: 0, + activeAlerts: [ + { key_as_string: '1608130800000', key: 1608130800000, doc_count: 0 }, + { key_as_string: '1608131400000', key: 1608131400000, doc_count: 1 }, + { key_as_string: '1608132000000', key: 1608132000000, doc_count: 1 }, + { key_as_string: '1608132600000', key: 1608132600000, doc_count: 1 }, + { key_as_string: '1608133200000', key: 1608133200000, doc_count: 1 }, + { key_as_string: '1608133800000', key: 1608133800000, doc_count: 1 }, + { key_as_string: '1608134400000', key: 1608134400000, doc_count: 1 }, + ], + recoveredAlerts: [ + { key_as_string: '2020-12-16T15:00:00.000Z', key: 1608130800000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:10:00.000Z', key: 1608131400000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:20:00.000Z', key: 1608132000000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:30:00.000Z', key: 1608132600000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:40:00.000Z', key: 1608133200000, doc_count: 0 }, + { key_as_string: '2020-12-16T15:50:00.000Z', key: 1608133800000, doc_count: 0 }, + { key_as_string: '2020-12-16T16:00:00.000Z', key: 1608134400000, doc_count: 0 }, + ], + }); + }); + }); +}; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts index 3d333d91da153..2ac420a8beb6b 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts @@ -20,7 +20,6 @@ export default ({ getService }: FtrProviderContext) => { const TEST_URL = '/internal/rac/alerts'; const ALERTS_INDEX_URL = `${TEST_URL}/index`; const SPACE1 = 'space1'; - const INDEX_ALIAS = '*'; const APM_ALERT_INDEX = '.alerts-observability.apm.alerts'; const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; @@ -54,12 +53,12 @@ export default ({ getService }: FtrProviderContext) => { describe('Users:', () => { it(`${obsOnlySpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => { const indexNames = await getAPMIndexName(obsOnlySpacesAll, SPACE1); - expect(indexNames.includes(`${APM_ALERT_INDEX}-${INDEX_ALIAS}`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below + expect(indexNames.includes(`${APM_ALERT_INDEX}-*`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below }); it(`${superUser.username} should be able to access the APM alert in ${SPACE1}`, async () => { const indexNames = await getAPMIndexName(superUser, SPACE1); - expect(indexNames.includes(`${APM_ALERT_INDEX}-${INDEX_ALIAS}`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below + expect(indexNames.includes(`${APM_ALERT_INDEX}-*`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below }); it(`${secOnlyRead.username} should NOT be able to access the APM alert in ${SPACE1}`, async () => { diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_browser_fields_by_feature_id.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_browser_fields_by_feature_id.ts index 6acbc14a47352..50504efd75ac0 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_browser_fields_by_feature_id.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_browser_fields_by_feature_id.ts @@ -46,7 +46,7 @@ export default ({ getService }: FtrProviderContext) => { 'logs', 'uptime', ]); - expect(Object.keys(browserFields)).to.eql(['base']); + expect(Object.keys(browserFields)).to.eql(['base', 'event', 'kibana', 'message']); }); it(`${superUser.username} should be able to get browser fields for o11y featureIds`, async () => { @@ -56,7 +56,7 @@ export default ({ getService }: FtrProviderContext) => { 'logs', 'uptime', ]); - expect(Object.keys(browserFields)).to.eql(['base']); + expect(Object.keys(browserFields)).to.eql(['base', 'event', 'kibana', 'message']); }); it(`${superUser.username} should NOT be able to get browser fields for siem featureId`, async () => { diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts index e1046b2dca6d7..8d71a4ce1f963 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts @@ -30,5 +30,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { loadTestFile(require.resolve('./find_alerts')); loadTestFile(require.resolve('./search_strategy')); loadTestFile(require.resolve('./get_browser_fields_by_feature_id')); + loadTestFile(require.resolve('./get_alert_summary')); }); };