mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[RAM] Add alert summary API (#146709)
## Summary Resolve: https://github.com/elastic/kibana/issues/141487 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
e43ccd13c6
commit
3917bf54c3
14 changed files with 614 additions and 7 deletions
|
@ -20,6 +20,7 @@ const createAlertsClientMock = () => {
|
|||
find: jest.fn(),
|
||||
getFeatureIdsByRegistrationContexts: jest.fn(),
|
||||
getBrowserFields: jest.fn(),
|
||||
getAlertSummary: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -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<Params extends RuleTypeParams = never>({
|
||||
id,
|
||||
status,
|
||||
|
|
|
@ -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\\"'"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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<RacRequestHandlerContext>) {
|
||||
getAlertByIdRoute(router);
|
||||
|
@ -23,4 +24,5 @@ export function defineRoutes(router: IRouter<RacRequestHandlerContext>) {
|
|||
findAlertsByQueryRoute(router);
|
||||
getFeatureIdsByRegistrationContexts(router);
|
||||
getBrowserFieldsByFeatureId(router);
|
||||
getAlertSummaryRoute(router);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ const readOperations: Record<AlertingEntity, string[]> = {
|
|||
'find',
|
||||
'getRuleExecutionKPI',
|
||||
],
|
||||
alert: ['get', 'find', 'getAuthorizedAlertsIndices'],
|
||||
alert: ['get', 'find', 'getAuthorizedAlertsIndices', 'getAlertSummary'],
|
||||
};
|
||||
|
||||
const writeOperations: Record<AlertingEntity, string[]> = {
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: ['*'],
|
||||
},
|
||||
|
|
|
@ -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 },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue