Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0bf35e3
add new API to get alert summary
XavierM Nov 30, 2022
54d2e06
Merge branch 'main' of github.com:elastic/kibana into api-alert-time-…
XavierM Nov 30, 2022
0110c54
Merge branch 'main' of github.com:elastic/kibana into api-alert-time-…
XavierM Nov 30, 2022
202cecb
review I
XavierM Dec 2, 2022
c383de7
Merge branch 'main' of github.com:elastic/kibana into api-alert-time-…
XavierM Dec 2, 2022
8f85cea
Merge branch 'main' of github.com:elastic/kibana into api-alert-time-…
XavierM Dec 6, 2022
4cf0bc0
update tests
XavierM Dec 6, 2022
6760d3d
Merge branch 'main' of github.com:elastic/kibana into api-alert-time-…
XavierM Dec 6, 2022
65a0536
Merge branch 'main' into api-alert-time-range
XavierM Dec 12, 2022
fa414eb
review I
XavierM Dec 12, 2022
12464f2
Merge branch 'main' of github.com:elastic/kibana into api-alert-time-…
XavierM Dec 12, 2022
d35f5fa
review II
XavierM Dec 13, 2022
df20fc2
Merge branch 'main' of github.com:elastic/kibana into api-alert-time-…
XavierM Dec 13, 2022
ce4707c
Merge branch 'main' into api-alert-time-range
XavierM Dec 13, 2022
eb96ff5
wip
XavierM Dec 14, 2022
4a0cb36
review III
XavierM Dec 16, 2022
f030944
Merge branch 'main' of github.com:elastic/kibana into api-alert-time-…
XavierM Dec 16, 2022
c39342b
test back
XavierM Dec 16, 2022
f892e9c
Merge branch 'main' of github.com:elastic/kibana into api-alert-time-…
XavierM Dec 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const createAlertsClientMock = () => {
find: jest.fn(),
getFeatureIdsByRegistrationContexts: jest.fn(),
getBrowserFields: jest.fn(),
getAlertSummary: jest.fn(),
};
return mocked;
};
Expand Down
124 changes: 124 additions & 0 deletions x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -92,6 +98,15 @@ interface GetAlertParams {
index?: string;
}

interface GetAlertSummaryParams {
id?: string;
gte: string;
lte: string;
featureIds: string[];
filter?: estypes.QueryDslQueryContainer[];
Comment thread
XavierM marked this conversation as resolved.
fixedInterval?: string;
}

interface SingleSearchAfterAndAudit {
id?: string | null | undefined;
query?: string | object | undefined;
Expand Down Expand Up @@ -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
Comment thread
XavierM marked this conversation as resolved.
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,
Expand Down
116 changes: 116 additions & 0 deletions x-pack/plugins/rule_registry/server/routes/get_alert_summary.test.ts
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\\"'"`
);
});
});
});
97 changes: 97 additions & 0 deletions x-pack/plugins/rule_registry/server/routes/get_alert_summary.ts
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'],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this option do?

Copy link
Copy Markdown
Contributor Author

@XavierM XavierM Dec 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for kibana security, I think we created a special rac access.

},
},
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() &&
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 moment.ISO_8601

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we validate this with a custom validator on the body params?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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,
},
},
});
}
}
);
};
2 changes: 2 additions & 0 deletions x-pack/plugins/rule_registry/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -23,4 +24,5 @@ export function defineRoutes(router: IRouter<RacRequestHandlerContext>) {
findAlertsByQueryRoute(router);
getFeatureIdsByRegistrationContexts(router);
getBrowserFieldsByFeatureId(router);
getAlertSummaryRoute(router);
}
Loading