-
Notifications
You must be signed in to change notification settings - Fork 8.2k
/
alerts_client.ts
215 lines (190 loc) · 6.55 KB
/
alerts_client.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/*
* 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 { PublicMethodsOf } from '@kbn/utility-types';
import { AlertTypeParams } from '../../../alerting/server';
import {
ReadOperations,
AlertingAuthorization,
WriteOperations,
AlertingAuthorizationEntity,
} from '../../../alerting/server';
import { Logger, ElasticsearchClient } from '../../../../../src/core/server';
import { alertAuditEvent, AlertAuditAction } from './audit_events';
import { AuditLogger } from '../../../security/server';
import { ALERT_STATUS, OWNER, RULE_ID } from '../../common/technical_rule_data_field_names';
import { ParsedTechnicalFields } from '../../common/parse_technical_fields';
// TODO: Fix typings https://github.com/elastic/kibana/issues/101776
type NonNullableProps<Obj extends {}, Props extends keyof Obj> = Omit<Obj, Props> &
{ [K in Props]-?: NonNullable<Obj[K]> };
type AlertType = NonNullableProps<ParsedTechnicalFields, 'rule.id' | 'kibana.rac.alert.owner'>;
const isValidAlert = (source?: ParsedTechnicalFields): source is AlertType => {
return source?.[RULE_ID] != null && source?.[OWNER] != null;
};
export interface ConstructorOptions {
logger: Logger;
authorization: PublicMethodsOf<AlertingAuthorization>;
auditLogger?: AuditLogger;
esClient: ElasticsearchClient;
}
export interface UpdateOptions<Params extends AlertTypeParams> {
id: string;
data: {
status: string;
};
index: string;
}
interface GetAlertParams {
id: string;
index?: string;
}
/**
* Provides apis to interact with alerts as data
* ensures the request is authorized to perform read / write actions
* on alerts as data.
*/
export class AlertsClient {
private readonly logger: Logger;
private readonly auditLogger?: AuditLogger;
private readonly authorization: PublicMethodsOf<AlertingAuthorization>;
private readonly esClient: ElasticsearchClient;
constructor({ auditLogger, authorization, logger, esClient }: ConstructorOptions) {
this.logger = logger;
this.authorization = authorization;
this.esClient = esClient;
this.auditLogger = auditLogger;
}
public async getAlertsIndex(featureIds: string[]) {
return this.authorization.getAugmentRuleTypesWithAuthorization(
featureIds.length !== 0 ? featureIds : ['apm', 'siem']
);
}
private async fetchAlert({ id, index }: GetAlertParams): Promise<AlertType> {
try {
const result = await this.esClient.search<ParsedTechnicalFields>({
// Context: Originally thought of always just searching `.alerts-*` but that could
// result in a big performance hit. If the client already knows which index the alert
// belongs to, passing in the index will speed things up
index: index ?? '.alerts-*',
body: { query: { term: { _id: id } } },
});
if (!isValidAlert(result.body.hits.hits[0]._source)) {
const errorMessage = `Unable to retrieve alert details for alert with id of "${id}".`;
this.logger.debug(errorMessage);
throw new Error(errorMessage);
}
return result.body.hits.hits[0]._source;
} catch (error) {
const errorMessage = `Unable to retrieve alert with id of "${id}".`;
this.logger.debug(errorMessage);
throw error;
}
}
public async get({ id, index }: GetAlertParams): Promise<ParsedTechnicalFields> {
try {
// first search for the alert by id, then use the alert info to check if user has access to it
const alert = await this.fetchAlert({
id,
index,
});
// this.authorization leverages the alerting plugin's authorization
// client exposed to us for reuse
await this.authorization.ensureAuthorized({
ruleTypeId: alert[RULE_ID],
consumer: alert[OWNER],
operation: ReadOperations.Get,
entity: AlertingAuthorizationEntity.Alert,
});
this.auditLogger?.log(
alertAuditEvent({
action: AlertAuditAction.GET,
id,
})
);
return alert;
} catch (error) {
this.logger.debug(`Error fetching alert with id of "${id}"`);
this.auditLogger?.log(
alertAuditEvent({
action: AlertAuditAction.GET,
id,
error,
})
);
throw error;
}
}
public async update<Params extends AlertTypeParams = never>({
id,
data,
index,
}: UpdateOptions<Params>): Promise<ParsedTechnicalFields | null | undefined> {
try {
const alert = await this.fetchAlert({
id,
index,
});
await this.authorization.ensureAuthorized({
ruleTypeId: alert[RULE_ID],
consumer: alert[OWNER],
operation: WriteOperations.Update,
entity: AlertingAuthorizationEntity.Alert,
});
const updateParameters = {
id,
index,
body: {
doc: {
[ALERT_STATUS]: data.status,
},
},
};
const res = await this.esClient.update<ParsedTechnicalFields, unknown, unknown, unknown>(
updateParameters
);
this.auditLogger?.log(
alertAuditEvent({
action: AlertAuditAction.UPDATE,
id,
})
);
return res.body.get?._source;
} catch (error) {
this.auditLogger?.log(
alertAuditEvent({
action: AlertAuditAction.UPDATE,
id,
error,
})
);
throw error;
}
}
public async getAuthorizedAlertsIndices(featureIds: string[]): Promise<string[] | undefined> {
const augmentedRuleTypes = await this.authorization.getAugmentRuleTypesWithAuthorization(
featureIds
);
const arrayOfAuthorizedRuleTypes = Array.from(augmentedRuleTypes.authorizedRuleTypes);
// As long as the user can read a minimum of one type of rule type produced by the provided feature,
// the user should be provided that features' alerts index.
// Limiting which alerts that user can read on that index will be done via the findAuthorizationFilter
const authorizedFeatures = arrayOfAuthorizedRuleTypes.reduce(
(acc, ruleType) => acc.add(ruleType.producer),
new Set<string>()
);
const toReturn = Array.from(authorizedFeatures).flatMap((feature) => {
switch (feature) {
case 'apm':
return '.alerts-observability-apm';
case 'siem':
return ['.alerts-security-solution', '.siem-signals'];
default:
return [];
}
});
return toReturn;
}
}