Skip to content

Commit 61113a0

Browse files
[ResponseOps][Alerting] Do not return alerts from internally managed rule types (elastic#223453)
## Summary This PR introduces the concept of internally managed rule types. The purpose of this PR is to hide alerts in the alerts table in the UI produced by internally managed rule types. In following PRs, we will enhance the framework to handle more cases when the product requirements are clearer. If, in the future, the streams team wants to use the alerts table to show stream alerts, we could introduce a new parameter in the alerting API to allow alerts produced by internally managed rule types to be returned. Fixes: elastic#221379 cc @kdelemme @dgieselaar ### 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 --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent b0aa031 commit 61113a0

9 files changed

Lines changed: 221 additions & 8 deletions

File tree

x-pack/platform/plugins/shared/alerting/README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,30 @@ Table of Contents
1414
- [Terminology](#terminology)
1515
- [Usage](#usage)
1616
- [Alerting API Keys](#alerting-api-keys)
17-
- [Plugin Status](#plugin-status)
1817
- [Rule Types](#rule-types)
1918
- [Methods](#methods)
20-
- [Alerts as Data](#alerts-as-data)
2119
- [Executor](#executor)
22-
- [Action variables](#action-variables)
20+
- [Alerts as Data](#alerts-as-data)
21+
- [Action Variables](#action-variables)
22+
- [useSavedObjectReferences Hooks](#usesavedobjectreferences-hooks)
2323
- [Recovered Alerts](#recovered-alerts)
2424
- [Licensing](#licensing)
2525
- [Documentation](#documentation)
2626
- [Tests](#tests)
2727
- [Example](#example)
2828
- [Role Based Access-Control](#role-based-access-control)
29-
- [Alerting Navigation](#alert-navigation)
29+
- [Subfeature privileges](#subfeature-privileges)
30+
- [`read` privileges vs. `all` privileges](#read-privileges-vs-all-privileges)
31+
- [Alert Navigation](#alert-navigation)
32+
- [registerNavigation](#registernavigation)
33+
- [registerDefaultNavigation](#registerdefaultnavigation)
34+
- [Balancing both APIs side by side](#balancing-both-apis-side-by-side)
3035
- [Internal HTTP APIs](#internal-http-apis)
3136
- [`GET /internal/alerting/rule/{id}/state`: Get rule state](#get-internalalertingruleidstate-get-rule-state)
32-
- [`GET /internal/alerting/rule/{id}/_alert_summary`: Get rule alert summary](#get-internalalertingruleidalertsummary-get-rule-alert-summary)
33-
- [`POST /api/alerting/rule/{id}/_update_api_key`: Update rule API key](#post-internalalertingruleidupdateapikey-update-rule-api-key)
37+
- [`GET /internal/alerting/rule/{id}/_alert_summary`: Get rule alert summary](#get-internalalertingruleid_alert_summary-get-rule-alert-summary)
38+
- [`POST /api/alerting/rule/{id}/_update_api_key`: Update rule API key](#post-apialertingruleid_update_api_key-update-rule-api-key)
3439
- [Alert Factory](#alert-factory)
40+
- [When should I use `setContext`?](#when-should-i-use-setcontext)
3541
- [Templating Actions](#templating-actions)
3642
- [Examples](#examples)
3743

@@ -102,6 +108,7 @@ The following table describes the properties of the `options` object.
102108
|alerts|(Optional) Specify options for writing alerts as data documents for this rule type. This feature is currently under development so this field is optional but we will eventually make this a requirement of all rule types. For full details, see the alerts as data section below.|IRuleTypeAlerts|
103109
|autoRecoverAlerts|(Optional) Whether the framework should determine if alerts have recovered between rule runs. If not specified, the default value of `true` is used. |boolean|
104110
|getViewInAppRelativeUrl|(Optional) When developing a rule type, you can choose to implement this hook for generating a link back to the Kibana application that can be used in alert actions. If not specified, a generic link back to the Rule Management app is generated.|Function|
111+
|internallyManaged|(Optional) Indicates that the rule type is managed internally by a Kibana plugin. Alerts of internally managed rule types are not returned by the APIs and thus not shown in the alerts table.|boolean|
105112

106113
### Executor
107114

x-pack/platform/plugins/shared/alerting/server/rule_type_registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export interface RegistryRuleType
7171
| 'defaultScheduleInterval'
7272
| 'doesSetRecoveryContext'
7373
| 'alerts'
74+
| 'internallyManaged'
7475
> {
7576
id: string;
7677
enabledInLicense: boolean;

x-pack/platform/plugins/shared/alerting/server/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,11 @@ export interface RuleType<
350350
*/
351351
autoRecoverAlerts?: boolean;
352352
getViewInAppRelativeUrl?: GetViewInAppRelativeUrlFn<Params>;
353+
/**
354+
* Indicates that the rule type is managed internally by a Kibana plugin.
355+
* Alerts of internally managed rule types are not returned by the APIs and thus not shown in the alerts table.
356+
*/
357+
internallyManaged?: boolean;
353358
}
354359
export type UntypedRuleType = RuleType<
355360
RuleTypeParams,

x-pack/platform/plugins/shared/rule_registry/server/search_strategy/search_strategy.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,4 +819,38 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
819819
})
820820
);
821821
});
822+
823+
it('removes internally managed rule types', async () => {
824+
const request: RuleRegistrySearchRequest = {
825+
ruleTypeIds: ['.es-query', '.internally-managed', '.not-internally-managed'],
826+
trackScores: true,
827+
};
828+
829+
const options = {};
830+
const deps = {
831+
request: {},
832+
};
833+
834+
getAuthorizedRuleTypesMock.mockResolvedValue([]);
835+
getAlertIndicesAliasMock.mockReturnValue(['security-siem']);
836+
alerting.listTypes.mockReturnValue(
837+
// @ts-expect-error: rule type properties are not needed for the test
838+
new Map([
839+
['.es-query', {}],
840+
['.internally-managed', { internallyManaged: true }],
841+
['.not-internally-managed', { internallyManaged: false }],
842+
])
843+
);
844+
845+
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
846+
847+
await lastValueFrom(
848+
strategy.search(request, options, deps as unknown as SearchStrategyDependencies)
849+
);
850+
851+
expect(authorizationMock.getAllAuthorizedRuleTypesFindOperation).toHaveBeenCalledWith({
852+
authorizationEntity: 'alert',
853+
ruleTypeIds: ['.es-query', '.not-internally-managed'],
854+
});
855+
});
822856
});

x-pack/platform/plugins/shared/rule_registry/server/search_strategy/search_strategy.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { map, mergeMap, catchError, of } from 'rxjs';
1010
import type { estypes } from '@elastic/elasticsearch';
1111
import type { Logger } from '@kbn/core/server';
1212
import { from } from 'rxjs';
13+
import type { RegistryRuleType } from '@kbn/alerting-plugin/server/rule_type_registry';
1314
import { ENHANCED_ES_SEARCH_STRATEGY } from '@kbn/data-plugin/common';
1415
import type { ISearchStrategy, PluginStart } from '@kbn/data-plugin/server';
1516
import type { AlertingServerStart } from '@kbn/alerting-plugin/server';
@@ -61,8 +62,11 @@ export const ruleRegistrySearchStrategyProvider = (
6162

6263
const registeredRuleTypes = alerting.listTypes();
6364

65+
const ruleTypesWithoutInternalRuleTypes =
66+
getRuleTypesWithoutInternalRuleTypes(registeredRuleTypes);
67+
6468
const [validRuleTypeIds, _] = partition(request.ruleTypeIds, (ruleTypeId) =>
65-
registeredRuleTypes.has(ruleTypeId)
69+
ruleTypesWithoutInternalRuleTypes.has(ruleTypeId)
6670
);
6771

6872
if (isAnyRuleTypeESAuthorized && !isEachRuleTypeESAuthorized) {
@@ -235,3 +239,11 @@ export const ruleRegistrySearchStrategyProvider = (
235239
},
236240
};
237241
};
242+
243+
const getRuleTypesWithoutInternalRuleTypes = (registeredRuleTypes: Map<string, RegistryRuleType>) =>
244+
new Map(
245+
Array.from(registeredRuleTypes).filter(
246+
([_id, ruleType]) =>
247+
ruleType.internallyManaged == null || !Boolean(ruleType.internallyManaged)
248+
)
249+
);

x-pack/platform/plugins/shared/streams/server/lib/rules/esql/register.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,6 @@ export function esqlRuleType(): PersistenceAlertType<
5555
shouldWrite: false,
5656
isSpaceAware: false,
5757
},
58+
internallyManaged: true,
5859
};
5960
}

x-pack/platform/test/alerting_api_integration/common/lib/alert_utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,3 +754,15 @@ function getAlwaysFiringRuleWithSystemAction(reference: string) {
754754
],
755755
};
756756
}
757+
758+
export function getAlwaysFiringInternalRule() {
759+
return {
760+
enabled: true,
761+
name: 'Internal Rule',
762+
schedule: { interval: '1m' },
763+
tags: [],
764+
rule_type_id: 'test.internal-rule-type',
765+
consumer: 'alertsFixture',
766+
params: {},
767+
};
768+
}

x-pack/platform/test/alerting_api_integration/common/plugins/alerts/server/rule_types.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,41 @@ function getSeverityRuleType() {
11921192
return result;
11931193
}
11941194

1195+
const getInternalRuleType = () => {
1196+
const result: RuleType<{}, never, {}, {}, {}, 'default'> = {
1197+
id: 'test.internal-rule-type',
1198+
name: 'Test: Internal Rule Type',
1199+
actionGroups: [{ id: 'default', name: 'Default' }],
1200+
validate: {
1201+
params: schema.any(),
1202+
},
1203+
category: 'management',
1204+
producer: 'alertsFixture',
1205+
solution: 'stack',
1206+
defaultActionGroupId: 'default',
1207+
minimumLicenseRequired: 'basic',
1208+
isExportable: true,
1209+
internallyManaged: true,
1210+
async executor(ruleExecutorOptions) {
1211+
const { services } = ruleExecutorOptions;
1212+
1213+
services.alertsClient?.report({ id: '1', actionGroup: 'default' });
1214+
services.alertsClient?.report({ id: '2', actionGroup: 'default' });
1215+
1216+
return { state: {} };
1217+
},
1218+
alerts: {
1219+
context: 'observability.test.alerts',
1220+
mappings: {
1221+
fieldMap: {},
1222+
},
1223+
useLegacyAlerts: true,
1224+
shouldWrite: true,
1225+
},
1226+
};
1227+
return result;
1228+
};
1229+
11951230
async function sendSignal(
11961231
logger: Logger,
11971232
es: ElasticsearchClient,
@@ -1531,4 +1566,5 @@ export function defineRuleTypes(
15311566
alerting.registerType(getPatternFiringAlertsAsDataRuleType());
15321567
alerting.registerType(getWaitingRuleType(logger));
15331568
alerting.registerType(getSeverityRuleType());
1569+
alerting.registerType(getInternalRuleType());
15341570
}

x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@
66
*/
77
import expect from '@kbn/expect';
88

9-
import { ALERT_START } from '@kbn/rule-data-utils';
9+
import { ALERT_RULE_TYPE_ID, ALERT_START } from '@kbn/rule-data-utils';
1010
import type { RuleRegistrySearchResponse } from '@kbn/rule-registry-plugin/common';
11+
import { ObjectRemover } from '@kbn/test-suites-xpack-platform/alerting_api_integration/common/lib';
12+
import { getAlwaysFiringInternalRule } from '@kbn/test-suites-xpack-platform/alerting_api_integration/common/lib/alert_utils';
13+
import { getEventLog } from '@kbn/test-suites-xpack-platform/alerting_api_integration/common/lib';
14+
import type { RetryService } from '@kbn/ftr-common-functional-services';
15+
import type { Client } from '@elastic/elasticsearch';
1116
import type { FtrProviderContext } from '../../../common/ftr_provider_context';
1217
import {
1318
obsOnlySpacesAll,
@@ -28,6 +33,9 @@ export default ({ getService }: FtrProviderContext) => {
2833
const supertestWithoutAuth = getService('supertestWithoutAuth');
2934
const secureSearch = getService('secureSearch');
3035
const kbnClient = getService('kibanaServer');
36+
const es = getService('es');
37+
const supertest = getService('supertest');
38+
const retry = getService('retry');
3139

3240
describe('ruleRegistryAlertsSearchStrategy', () => {
3341
let kibanaVersion: string;
@@ -983,6 +991,55 @@ export default ({ getService }: FtrProviderContext) => {
983991
expect(result.rawResponse.hits.total).to.eql(0);
984992
});
985993
});
994+
995+
describe('internal rule types', () => {
996+
const alertAsDataIndex = '.internal.alerts-observability.test.alerts.alerts-default-000001';
997+
const objectRemover = new ObjectRemover(supertest);
998+
const rulePayload = getAlwaysFiringInternalRule();
999+
let ruleId: string;
1000+
1001+
before(async () => {
1002+
await deleteAllAlertsFromIndex(alertAsDataIndex, es);
1003+
});
1004+
1005+
beforeEach(async () => {
1006+
const { body: createdRule1 } = await supertest
1007+
.post('/api/alerting/rule')
1008+
.set('kbn-xsrf', 'foo')
1009+
.send(rulePayload)
1010+
.expect(200);
1011+
1012+
ruleId = createdRule1.id;
1013+
objectRemover.add('default', createdRule1.id, 'rule', 'alerting');
1014+
});
1015+
1016+
afterEach(async () => {
1017+
await deleteAllAlertsFromIndex(alertAsDataIndex, es);
1018+
await objectRemover.removeAll();
1019+
});
1020+
1021+
it('should not return alerts from internal rule types', async () => {
1022+
await waitForRuleExecution(retry, getService, ruleId);
1023+
await waitForActiveAlerts(es, retry, alertAsDataIndex, rulePayload.rule_type_id);
1024+
1025+
const result = await secureSearch.send<RuleRegistrySearchResponse>({
1026+
supertestWithoutAuth,
1027+
auth: {
1028+
username: superUser.username,
1029+
password: superUser.password,
1030+
},
1031+
referer: 'test',
1032+
internalOrigin: 'Kibana',
1033+
options: {
1034+
ruleTypeIds: [rulePayload.rule_type_id],
1035+
},
1036+
strategy: 'privateRuleRegistryAlertsSearchStrategy',
1037+
});
1038+
1039+
expect(result.rawResponse.hits.total).to.eql(0);
1040+
expect(result.rawResponse.hits.hits.length).to.eql(0);
1041+
});
1042+
});
9861043
});
9871044
};
9881045

@@ -1001,3 +1058,51 @@ const validateRuleTypeIds = (result: RuleRegistrySearchResponse, ruleTypeIdsToVe
10011058
)
10021059
).to.eql(true);
10031060
};
1061+
1062+
const waitForRuleExecution = async (
1063+
retry: RetryService,
1064+
getService: FtrProviderContext['getService'],
1065+
ruleId: string
1066+
) => {
1067+
return await retry.try(async () => {
1068+
await getEventLog({
1069+
getService,
1070+
spaceId: 'default',
1071+
type: 'alert',
1072+
id: ruleId,
1073+
provider: 'alerting',
1074+
actions: new Map([['active-instance', { gte: 1 }]]),
1075+
});
1076+
});
1077+
};
1078+
1079+
const waitForActiveAlerts = async (
1080+
es: Client,
1081+
retry: RetryService,
1082+
alertAsDataIndex: string,
1083+
ruleTypeId: string
1084+
) => {
1085+
await retry.try(async () => {
1086+
const {
1087+
hits: { hits: activeAlerts },
1088+
} = await es.search({
1089+
index: alertAsDataIndex,
1090+
query: { match_all: {} },
1091+
});
1092+
1093+
activeAlerts.forEach((activeAlert: any) => {
1094+
expect(activeAlert._source[ALERT_RULE_TYPE_ID]).eql(ruleTypeId);
1095+
});
1096+
});
1097+
};
1098+
1099+
const deleteAllAlertsFromIndex = async (index: string, es: Client) => {
1100+
await es.deleteByQuery({
1101+
index,
1102+
query: {
1103+
match_all: {},
1104+
},
1105+
conflicts: 'proceed',
1106+
ignore_unavailable: true,
1107+
});
1108+
};

0 commit comments

Comments
 (0)