Skip to content

Commit cf4f67b

Browse files
add workflow tags to bulk update schema
1 parent b1470db commit cf4f67b

File tree

3 files changed

+148
-54
lines changed

3 files changed

+148
-54
lines changed

x-pack/platform/plugins/shared/rule_registry/server/alert_data_client/alerts_client.ts

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ import { getRuleTypeIdsFilter } from '../lib/get_rule_type_ids_filter';
7070
import { getConsumersFilter } from '../lib/get_consumers_filter';
7171
import { mergeUniqueFieldsByName } from '../utils/unique_fields';
7272
import { getAlertFieldsFromIndexFetcher } from '../utils/get_alert_fields_from_index_fetcher';
73+
import {
74+
ADD_TAGS_UPDATE_SCRIPT,
75+
getStatusUpdateScript,
76+
REMOVE_TAGS_UPDATE_SCRIPT,
77+
} from '../utils/alert_client_bulk_update_scripts';
7378
import type { GetAlertFieldsResponseV1 } from '../routes/get_alert_fields';
7479

7580
// TODO: Fix typings https://github.com/elastic/kibana/issues/101776
@@ -113,9 +118,11 @@ export interface UpdateOptions<Params extends RuleTypeParams> {
113118

114119
export interface BulkUpdateOptions<Params extends RuleTypeParams> {
115120
ids?: string[] | null;
116-
status: STATUS_VALUES;
121+
status?: STATUS_VALUES;
117122
index: string;
118123
query?: object | string | null;
124+
addTags?: string[];
125+
removeTags?: string[];
119126
}
120127

121128
interface MgetAndAuditAlert {
@@ -442,25 +449,6 @@ export class AlertsClient {
442449
}
443450
}
444451

445-
/**
446-
* When an update by ids is requested, do a multi-get, ensure authz and audit alerts, then execute bulk update
447-
*/
448-
private async mgetAlertsAuditOperateStatus({
449-
alerts,
450-
status,
451-
operation,
452-
}: {
453-
alerts: MgetAndAuditAlert[];
454-
status: STATUS_VALUES;
455-
operation: ReadOperations.Find | ReadOperations.Get | WriteOperations.Update;
456-
}) {
457-
return this.mgetAlertsAuditOperate({
458-
alerts,
459-
operation,
460-
fieldToUpdate: (source) => this.getAlertStatusFieldUpdate(source, status),
461-
});
462-
}
463-
464452
private async buildEsQueryWithAuthz(
465453
query: object | string | null | undefined,
466454
id: string | null | undefined,
@@ -810,15 +798,70 @@ export class AlertsClient {
810798
query,
811799
index,
812800
status,
801+
addTags,
802+
removeTags,
813803
}: BulkUpdateOptions<Params>) {
804+
const scriptOps: string[] = [];
805+
const params: Record<string, any> = {};
806+
807+
if (status != null) {
808+
scriptOps.push(getStatusUpdateScript(status));
809+
}
810+
811+
if (addTags != null && addTags.length > 0) {
812+
params.addTags = addTags;
813+
scriptOps.push(ADD_TAGS_UPDATE_SCRIPT);
814+
}
815+
816+
if (removeTags != null && removeTags.length > 0) {
817+
params.removeTags = removeTags;
818+
scriptOps.push(REMOVE_TAGS_UPDATE_SCRIPT);
819+
}
820+
821+
if (scriptOps.length === 0) {
822+
return;
823+
}
824+
825+
const script = {
826+
source: scriptOps.join('\n'),
827+
lang: 'painless',
828+
params,
829+
};
830+
814831
// rejects at the route level if more than 1000 id's are passed in
815832
if (ids != null) {
816833
const alerts = ids.map((id) => ({ id, index }));
817-
return this.mgetAlertsAuditOperateStatus({
834+
const mgetRes = await this.ensureAllAlertsAuthorized({
818835
alerts,
819-
status,
820836
operation: WriteOperations.Update,
821837
});
838+
839+
const bulkUpdateRequest = [];
840+
841+
for (const item of mgetRes.docs) {
842+
// @ts-expect-error doesn't handle error branch in MGetResponse
843+
if (item.found) {
844+
bulkUpdateRequest.push(
845+
{
846+
update: {
847+
_index: item._index,
848+
_id: item._id,
849+
},
850+
},
851+
{ script }
852+
);
853+
}
854+
}
855+
856+
if (bulkUpdateRequest.length === 0) {
857+
return;
858+
}
859+
860+
const bulkUpdateResponse = await this.esClient.bulk({
861+
refresh: 'wait_for',
862+
body: bulkUpdateRequest,
863+
});
864+
return bulkUpdateResponse;
822865
} else if (query != null) {
823866
try {
824867
// execute search after with query + authorization filter
@@ -835,18 +878,11 @@ export class AlertsClient {
835878

836879
// executes updateByQuery with query + authorization filter
837880
// used in the queryAndAuditAllAlerts function
881+
838882
const result = await this.esClient.updateByQuery({
839883
index,
840884
conflicts: 'proceed',
841-
script: {
842-
source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) {
843-
ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}'
844-
}
845-
if (ctx._source.signal != null && ctx._source.signal.status != null) {
846-
ctx._source.signal.status = '${status}'
847-
}`,
848-
lang: 'painless',
849-
},
885+
script,
850886
query: fetchAndAuditResponse.authorizedQuery as Omit<QueryDslQueryContainer, 'script'>,
851887
ignore_unavailable: true,
852888
});

x-pack/platform/plugins/shared/rule_registry/server/routes/bulk_update_alerts.ts

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,40 @@ export const bulkUpdateAlertsRoute = (router: IRouter<RacRequestHandlerContext>)
2020
validate: {
2121
body: buildRouteValidation(
2222
t.union([
23-
t.strict({
24-
status: t.union([
25-
t.literal('open'),
26-
t.literal('closed'),
27-
t.literal('in-progress'), // TODO: remove after migration to acknowledged
28-
t.literal('acknowledged'),
29-
]),
30-
index: t.string,
31-
ids: t.array(t.string),
32-
query: t.undefined,
33-
}),
34-
t.strict({
35-
status: t.union([
36-
t.literal('open'),
37-
t.literal('closed'),
38-
t.literal('in-progress'), // TODO: remove after migration to acknowledged
39-
t.literal('acknowledged'),
40-
]),
41-
index: t.string,
42-
ids: t.undefined,
43-
query: t.union([t.object, t.string]),
44-
}),
23+
t.intersection([
24+
t.strict({
25+
index: t.string,
26+
ids: t.array(t.string),
27+
query: t.undefined,
28+
}),
29+
t.partial({
30+
status: t.union([
31+
t.literal('open'),
32+
t.literal('closed'),
33+
t.literal('in-progress'), // TODO: remove after migration to acknowledged
34+
t.literal('acknowledged'),
35+
]),
36+
addTags: t.array(t.string),
37+
removeTags: t.array(t.string),
38+
}),
39+
]),
40+
t.intersection([
41+
t.strict({
42+
index: t.string,
43+
ids: t.undefined,
44+
query: t.union([t.object, t.string]),
45+
}),
46+
t.partial({
47+
status: t.union([
48+
t.literal('open'),
49+
t.literal('closed'),
50+
t.literal('in-progress'), // TODO: remove after migration to acknowledged
51+
t.literal('acknowledged'),
52+
]),
53+
addTags: t.array(t.string),
54+
removeTags: t.array(t.string),
55+
}),
56+
]),
4557
])
4658
),
4759
},
@@ -58,7 +70,7 @@ export const bulkUpdateAlertsRoute = (router: IRouter<RacRequestHandlerContext>)
5870
try {
5971
const racContext = await context.rac;
6072
const alertsClient = await racContext.getAlertsClient();
61-
const { status, ids, index, query } = req.body;
73+
const { status, ids, index, query, addTags, removeTags } = req.body;
6274

6375
if (ids != null && ids.length > 1000) {
6476
return response.badRequest({
@@ -73,6 +85,8 @@ export const bulkUpdateAlertsRoute = (router: IRouter<RacRequestHandlerContext>)
7385
status,
7486
query,
7587
index,
88+
addTags,
89+
removeTags,
7690
});
7791

7892
if (updatedAlert == null) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import {
9+
ALERT_WORKFLOW_STATUS,
10+
ALERT_WORKFLOW_TAGS,
11+
} from '../../common/technical_rule_data_field_names';
12+
13+
export const getStatusUpdateScript = (status: string) => {
14+
return `
15+
if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) {
16+
ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}';
17+
}
18+
if (ctx._source.signal != null && ctx._source.signal.status != null) {
19+
ctx._source.signal.status = '${status}';
20+
}
21+
`;
22+
};
23+
24+
export const ADD_TAGS_UPDATE_SCRIPT = `
25+
if (ctx._source['${ALERT_WORKFLOW_TAGS}'] == null) {
26+
ctx._source['${ALERT_WORKFLOW_TAGS}'] = new ArrayList();
27+
}
28+
for (item in params.addTags) {
29+
if (!ctx._source['${ALERT_WORKFLOW_TAGS}'].contains(item)) {
30+
ctx._source['${ALERT_WORKFLOW_TAGS}'].add(item);
31+
}
32+
}
33+
`;
34+
35+
export const REMOVE_TAGS_UPDATE_SCRIPT = `
36+
if (ctx._source['${ALERT_WORKFLOW_TAGS}'] != null) {
37+
for (int i = 0; i < params.removeTags.length; i++) {
38+
if (ctx._source['${ALERT_WORKFLOW_TAGS}'].contains(params.removeTags[i])) {
39+
int index = ctx._source['${ALERT_WORKFLOW_TAGS}'].indexOf(params.removeTags[i]);
40+
ctx._source['${ALERT_WORKFLOW_TAGS}'].remove(index);
41+
}
42+
}
43+
}
44+
`;

0 commit comments

Comments
 (0)