mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[RAC] [RBAC] MVP RBAC for alerts as data (#100705)
An MVP of the RBAC work required for the "alerts as data" effort. An example of the existing implementation for alerts would be that of the security solution. The security solution stores its alerts generated from rules in a single data index - .siem-signals. In order to gain or restrict access to alerts, users do so by following the Elasticsearch privilege architecture. A user would need to go into the Kibana role access UI and give explicit read/write/manage permissions for the index itself. Kibana as a whole is moving away from this model and instead having all user interactions run through the Kibana privilege model. When solutions use saved objects, this authentication layer is abstracted away for them. Because we have chosen to use data indices for alerts, we cannot rely on this abstracted out layer that saved objects provide - we need to provide our own RBAC! Instead of giving users explicit permission to an alerts index, users are instead given access to features. They don't need to know anything about indices, that work we do under the covers now. Co-authored-by: Yara Tercero <yctercero@users.noreply.github.com> Co-authored-by: Yara Tercero <yara.tercero@elastic.co>
This commit is contained in:
parent
e9ec16ec97
commit
c77c7fbedb
118 changed files with 5700 additions and 106 deletions
|
@ -19,6 +19,7 @@ const RULE_NAME = 'rule.name' as const;
|
|||
const RULE_CATEGORY = 'rule.category' as const;
|
||||
const TAGS = 'tags' as const;
|
||||
const PRODUCER = `${ALERT_NAMESPACE}.producer` as const;
|
||||
const OWNER = `${ALERT_NAMESPACE}.owner` as const;
|
||||
const ALERT_ID = `${ALERT_NAMESPACE}.id` as const;
|
||||
const ALERT_UUID = `${ALERT_NAMESPACE}.uuid` as const;
|
||||
const ALERT_START = `${ALERT_NAMESPACE}.start` as const;
|
||||
|
@ -40,6 +41,7 @@ const fields = {
|
|||
RULE_CATEGORY,
|
||||
TAGS,
|
||||
PRODUCER,
|
||||
OWNER,
|
||||
ALERT_ID,
|
||||
ALERT_UUID,
|
||||
ALERT_START,
|
||||
|
@ -62,6 +64,7 @@ export {
|
|||
RULE_CATEGORY,
|
||||
TAGS,
|
||||
PRODUCER,
|
||||
OWNER,
|
||||
ALERT_ID,
|
||||
ALERT_UUID,
|
||||
ALERT_START,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
// Similar to the src/core/server/saved_objects/version/decode_version.ts
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
|
@ -8,10 +8,12 @@
|
|||
|
||||
export * from './bad_request_error';
|
||||
export * from './create_boostrap_index';
|
||||
export * from './decode_version';
|
||||
export * from './delete_all_index';
|
||||
export * from './delete_policy';
|
||||
export * from './delete_template';
|
||||
export * from './elasticsearch_client';
|
||||
export * from './encode_hit_version';
|
||||
export * from './get_index_aliases';
|
||||
export * from './get_index_count';
|
||||
export * from './get_index_exists';
|
||||
|
|
|
@ -16,12 +16,13 @@ const createAlertingAuthorizationMock = () => {
|
|||
ensureAuthorized: jest.fn(),
|
||||
filterByRuleTypeAuthorization: jest.fn(),
|
||||
getFindAuthorizationFilter: jest.fn(),
|
||||
getAugmentedRuleTypesWithAuthorization: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const alertingAuthorizationMock: {
|
||||
create: () => AlertingAuthorizationMock;
|
||||
create: () => jest.Mocked<PublicMethodsOf<AlertingAuthorization>>;
|
||||
} = {
|
||||
create: createAlertingAuthorizationMock,
|
||||
};
|
||||
|
|
|
@ -1944,4 +1944,184 @@ describe('AlertingAuthorization', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAugmentedRuleTypesWithAuthorization', () => {
|
||||
const myOtherAppAlertType: RegistryAlertType = {
|
||||
actionGroups: [],
|
||||
actionVariables: undefined,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
id: 'myOtherAppAlertType',
|
||||
name: 'myOtherAppAlertType',
|
||||
producer: 'alerts',
|
||||
enabledInLicense: true,
|
||||
isExportable: true,
|
||||
};
|
||||
const myAppAlertType: RegistryAlertType = {
|
||||
actionGroups: [],
|
||||
actionVariables: undefined,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
id: 'myAppAlertType',
|
||||
name: 'myAppAlertType',
|
||||
producer: 'myApp',
|
||||
enabledInLicense: true,
|
||||
isExportable: true,
|
||||
};
|
||||
const mySecondAppAlertType: RegistryAlertType = {
|
||||
actionGroups: [],
|
||||
actionVariables: undefined,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
id: 'mySecondAppAlertType',
|
||||
name: 'mySecondAppAlertType',
|
||||
producer: 'myApp',
|
||||
enabledInLicense: true,
|
||||
isExportable: true,
|
||||
};
|
||||
const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]);
|
||||
|
||||
test('it returns authorized rule types given a set of feature ids', async () => {
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<
|
||||
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
|
||||
> = jest.fn();
|
||||
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
|
||||
checkPrivileges.mockResolvedValueOnce({
|
||||
username: 'some-user',
|
||||
hasAllRequested: false,
|
||||
privileges: {
|
||||
kibana: [
|
||||
{
|
||||
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'),
|
||||
authorized: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const alertAuthorization = new AlertingAuthorization({
|
||||
request,
|
||||
authorization,
|
||||
alertTypeRegistry,
|
||||
features,
|
||||
auditLogger,
|
||||
getSpace,
|
||||
exemptConsumerIds,
|
||||
});
|
||||
alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
|
||||
|
||||
await expect(
|
||||
alertAuthorization.getAugmentedRuleTypesWithAuthorization(
|
||||
['myApp'],
|
||||
[ReadOperations.Find, ReadOperations.Get, WriteOperations.Update],
|
||||
AlertingAuthorizationEntity.Alert
|
||||
)
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"authorizedRuleTypes": Set {
|
||||
Object {
|
||||
"actionGroups": Array [],
|
||||
"actionVariables": undefined,
|
||||
"authorizedConsumers": Object {
|
||||
"myApp": Object {
|
||||
"all": false,
|
||||
"read": true,
|
||||
},
|
||||
},
|
||||
"defaultActionGroupId": "default",
|
||||
"enabledInLicense": true,
|
||||
"id": "myOtherAppAlertType",
|
||||
"isExportable": true,
|
||||
"minimumLicenseRequired": "basic",
|
||||
"name": "myOtherAppAlertType",
|
||||
"producer": "alerts",
|
||||
"recoveryActionGroup": Object {
|
||||
"id": "recovered",
|
||||
"name": "Recovered",
|
||||
},
|
||||
},
|
||||
},
|
||||
"hasAllRequested": false,
|
||||
"username": "some-user",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('it returns all authorized if user has read, get and update alert privileges', async () => {
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<
|
||||
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
|
||||
> = jest.fn();
|
||||
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
|
||||
checkPrivileges.mockResolvedValueOnce({
|
||||
username: 'some-user',
|
||||
hasAllRequested: false,
|
||||
privileges: {
|
||||
kibana: [
|
||||
{
|
||||
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'),
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'get'),
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'update'),
|
||||
authorized: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const alertAuthorization = new AlertingAuthorization({
|
||||
request,
|
||||
authorization,
|
||||
alertTypeRegistry,
|
||||
features,
|
||||
auditLogger,
|
||||
getSpace,
|
||||
exemptConsumerIds,
|
||||
});
|
||||
alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);
|
||||
|
||||
await expect(
|
||||
alertAuthorization.getAugmentedRuleTypesWithAuthorization(
|
||||
['myApp'],
|
||||
[ReadOperations.Find, ReadOperations.Get, WriteOperations.Update],
|
||||
AlertingAuthorizationEntity.Alert
|
||||
)
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"authorizedRuleTypes": Set {
|
||||
Object {
|
||||
"actionGroups": Array [],
|
||||
"actionVariables": undefined,
|
||||
"authorizedConsumers": Object {
|
||||
"myApp": Object {
|
||||
"all": true,
|
||||
"read": true,
|
||||
},
|
||||
},
|
||||
"defaultActionGroupId": "default",
|
||||
"enabledInLicense": true,
|
||||
"id": "myOtherAppAlertType",
|
||||
"isExportable": true,
|
||||
"minimumLicenseRequired": "basic",
|
||||
"name": "myOtherAppAlertType",
|
||||
"producer": "alerts",
|
||||
"recoveryActionGroup": Object {
|
||||
"id": "recovered",
|
||||
"name": "Recovered",
|
||||
},
|
||||
},
|
||||
},
|
||||
"hasAllRequested": false,
|
||||
"username": "some-user",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -124,20 +124,41 @@ export class AlertingAuthorization {
|
|||
return new Set();
|
||||
});
|
||||
|
||||
this.allPossibleConsumers = this.featuresIds.then((featuresIds) =>
|
||||
featuresIds.size
|
||||
this.allPossibleConsumers = this.featuresIds.then((featuresIds) => {
|
||||
return featuresIds.size
|
||||
? asAuthorizedConsumers([...this.exemptConsumerIds, ...featuresIds], {
|
||||
read: true,
|
||||
all: true,
|
||||
})
|
||||
: {}
|
||||
);
|
||||
: {};
|
||||
});
|
||||
}
|
||||
|
||||
private shouldCheckAuthorization(): boolean {
|
||||
return this.authorization?.mode?.useRbacForRequest(this.request) ?? false;
|
||||
}
|
||||
|
||||
/*
|
||||
* This method exposes the private 'augmentRuleTypesWithAuthorization' to be
|
||||
* used by the RAC/Alerts client
|
||||
*/
|
||||
public async getAugmentedRuleTypesWithAuthorization(
|
||||
featureIds: readonly string[],
|
||||
operations: Array<ReadOperations | WriteOperations>,
|
||||
authorizationEntity: AlertingAuthorizationEntity
|
||||
): Promise<{
|
||||
username?: string;
|
||||
hasAllRequested: boolean;
|
||||
authorizedRuleTypes: Set<RegistryAlertTypeWithAuth>;
|
||||
}> {
|
||||
return this.augmentRuleTypesWithAuthorization(
|
||||
this.alertTypeRegistry.list(),
|
||||
operations,
|
||||
authorizationEntity,
|
||||
new Set(featureIds)
|
||||
);
|
||||
}
|
||||
|
||||
public async ensureAuthorized({ ruleTypeId, consumer, operation, entity }: EnsureAuthorizedOpts) {
|
||||
const { authorization } = this;
|
||||
|
||||
|
@ -339,13 +360,14 @@ export class AlertingAuthorization {
|
|||
private async augmentRuleTypesWithAuthorization(
|
||||
ruleTypes: Set<RegistryAlertType>,
|
||||
operations: Array<ReadOperations | WriteOperations>,
|
||||
authorizationEntity: AlertingAuthorizationEntity
|
||||
authorizationEntity: AlertingAuthorizationEntity,
|
||||
featuresIds?: Set<string>
|
||||
): Promise<{
|
||||
username?: string;
|
||||
hasAllRequested: boolean;
|
||||
authorizedRuleTypes: Set<RegistryAlertTypeWithAuth>;
|
||||
}> {
|
||||
const featuresIds = await this.featuresIds;
|
||||
const fIds = featuresIds ?? (await this.featuresIds);
|
||||
if (this.authorization && this.shouldCheckAuthorization()) {
|
||||
const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(
|
||||
this.request
|
||||
|
@ -363,7 +385,7 @@ export class AlertingAuthorization {
|
|||
// as we can't ask ES for the user's individual privileges we need to ask for each feature
|
||||
// and ruleType in the system whether this user has this privilege
|
||||
for (const ruleType of ruleTypesWithAuthorization) {
|
||||
for (const feature of featuresIds) {
|
||||
for (const feature of fIds) {
|
||||
for (const operation of operations) {
|
||||
privilegeToRuleType.set(
|
||||
this.authorization!.actions.alerting.get(
|
||||
|
@ -420,7 +442,7 @@ export class AlertingAuthorization {
|
|||
return {
|
||||
hasAllRequested: true,
|
||||
authorizedRuleTypes: this.augmentWithAuthorizedConsumers(
|
||||
new Set([...ruleTypes].filter((ruleType) => featuresIds.has(ruleType.producer))),
|
||||
new Set([...ruleTypes].filter((ruleType) => fIds.has(ruleType.producer))),
|
||||
await this.allPossibleConsumers
|
||||
),
|
||||
};
|
||||
|
|
|
@ -34,6 +34,13 @@ export { FindResult } from './alerts_client';
|
|||
export { PublicAlertInstance as AlertInstance } from './alert_instance';
|
||||
export { parseDuration } from './lib';
|
||||
export { getEsErrorMessage } from './lib/errors';
|
||||
export {
|
||||
ReadOperations,
|
||||
AlertingAuthorizationFilterType,
|
||||
AlertingAuthorization,
|
||||
WriteOperations,
|
||||
AlertingAuthorizationEntity,
|
||||
} from './authorization';
|
||||
|
||||
export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext);
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import type { ValuesType } from 'utility-types';
|
|||
import type { ActionGroup } from '../../alerting/common';
|
||||
import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from './ml_constants';
|
||||
|
||||
export const APM_SERVER_FEATURE_ID = 'apm';
|
||||
|
||||
export enum AlertType {
|
||||
ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat.
|
||||
TransactionErrorRate = 'apm.transaction_error_rate',
|
||||
|
@ -44,7 +46,7 @@ export const ALERT_TYPES_CONFIG: Record<
|
|||
actionGroups: [THRESHOLD_MET_GROUP],
|
||||
defaultActionGroupId: THRESHOLD_MET_GROUP_ID,
|
||||
minimumLicenseRequired: 'basic',
|
||||
producer: 'apm',
|
||||
producer: APM_SERVER_FEATURE_ID,
|
||||
isExportable: true,
|
||||
},
|
||||
[AlertType.TransactionDuration]: {
|
||||
|
@ -54,7 +56,7 @@ export const ALERT_TYPES_CONFIG: Record<
|
|||
actionGroups: [THRESHOLD_MET_GROUP],
|
||||
defaultActionGroupId: THRESHOLD_MET_GROUP_ID,
|
||||
minimumLicenseRequired: 'basic',
|
||||
producer: 'apm',
|
||||
producer: APM_SERVER_FEATURE_ID,
|
||||
isExportable: true,
|
||||
},
|
||||
[AlertType.TransactionDurationAnomaly]: {
|
||||
|
@ -64,7 +66,7 @@ export const ALERT_TYPES_CONFIG: Record<
|
|||
actionGroups: [THRESHOLD_MET_GROUP],
|
||||
defaultActionGroupId: THRESHOLD_MET_GROUP_ID,
|
||||
minimumLicenseRequired: 'basic',
|
||||
producer: 'apm',
|
||||
producer: APM_SERVER_FEATURE_ID,
|
||||
isExportable: true,
|
||||
},
|
||||
[AlertType.TransactionErrorRate]: {
|
||||
|
@ -74,7 +76,7 @@ export const ALERT_TYPES_CONFIG: Record<
|
|||
actionGroups: [THRESHOLD_MET_GROUP],
|
||||
defaultActionGroupId: THRESHOLD_MET_GROUP_ID,
|
||||
minimumLicenseRequired: 'basic',
|
||||
producer: 'apm',
|
||||
producer: APM_SERVER_FEATURE_ID,
|
||||
isExportable: true,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { AlertType } from '../../../../common/alert_types';
|
||||
import {
|
||||
AlertType,
|
||||
APM_SERVER_FEATURE_ID,
|
||||
} from '../../../../common/alert_types';
|
||||
import { getInitialAlertValues } from '../get_initial_alert_values';
|
||||
import { ApmPluginStartDeps } from '../../../plugin';
|
||||
interface Props {
|
||||
|
@ -31,7 +34,7 @@ export function AlertingFlyout(props: Props) {
|
|||
() =>
|
||||
alertType &&
|
||||
services.triggersActionsUi.getAddAlertFlyout({
|
||||
consumer: 'apm',
|
||||
consumer: APM_SERVER_FEATURE_ID,
|
||||
onClose: onCloseAddFlyout,
|
||||
alertTypeId: alertType,
|
||||
canChangeTrigger: false,
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SubFeaturePrivilegeGroupType } from '../../features/common';
|
||||
import { LicenseType } from '../../licensing/common/types';
|
||||
import { AlertType } from '../common/alert_types';
|
||||
import { AlertType, APM_SERVER_FEATURE_ID } from '../common/alert_types';
|
||||
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
|
||||
import {
|
||||
LicensingPluginSetup,
|
||||
|
@ -15,14 +16,14 @@ import {
|
|||
} from '../../licensing/server';
|
||||
|
||||
export const APM_FEATURE = {
|
||||
id: 'apm',
|
||||
id: APM_SERVER_FEATURE_ID,
|
||||
name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', {
|
||||
defaultMessage: 'APM and User Experience',
|
||||
}),
|
||||
order: 900,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
app: ['apm', 'ux', 'kibana'],
|
||||
catalogue: ['apm'],
|
||||
app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'],
|
||||
catalogue: [APM_SERVER_FEATURE_ID],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
|
@ -30,9 +31,9 @@ export const APM_FEATURE = {
|
|||
// see x-pack/plugins/features/common/feature_kibana_privileges.ts
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['apm', 'ux', 'kibana'],
|
||||
api: ['apm', 'apm_write'],
|
||||
catalogue: ['apm'],
|
||||
app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'],
|
||||
api: [APM_SERVER_FEATURE_ID, 'apm_write', 'rac'],
|
||||
catalogue: [APM_SERVER_FEATURE_ID],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
@ -41,9 +42,6 @@ export const APM_FEATURE = {
|
|||
rule: {
|
||||
all: Object.values(AlertType),
|
||||
},
|
||||
alert: {
|
||||
all: Object.values(AlertType),
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
|
@ -51,9 +49,9 @@ export const APM_FEATURE = {
|
|||
ui: ['show', 'save', 'alerting:show', 'alerting:save'],
|
||||
},
|
||||
read: {
|
||||
app: ['apm', 'ux', 'kibana'],
|
||||
api: ['apm'],
|
||||
catalogue: ['apm'],
|
||||
app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'],
|
||||
api: [APM_SERVER_FEATURE_ID, 'rac'],
|
||||
catalogue: [APM_SERVER_FEATURE_ID],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
@ -62,9 +60,6 @@ export const APM_FEATURE = {
|
|||
rule: {
|
||||
read: Object.values(AlertType),
|
||||
},
|
||||
alert: {
|
||||
read: Object.values(AlertType),
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
|
@ -72,6 +67,60 @@ export const APM_FEATURE = {
|
|||
ui: ['show', 'alerting:show', 'alerting:save'],
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.apm.featureRegistry.manageAlertsName', {
|
||||
defaultMessage: 'Alerts',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'mutually_exclusive' as SubFeaturePrivilegeGroupType,
|
||||
privileges: [
|
||||
{
|
||||
id: 'alerts_all',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.featureRegistry.subfeature.alertsAllName',
|
||||
{
|
||||
defaultMessage: 'All',
|
||||
}
|
||||
),
|
||||
includeIn: 'all' as 'all',
|
||||
alerting: {
|
||||
alert: {
|
||||
all: Object.values(AlertType),
|
||||
},
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
{
|
||||
id: 'alerts_read',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.featureRegistry.subfeature.alertsReadName',
|
||||
{
|
||||
defaultMessage: 'Read',
|
||||
}
|
||||
),
|
||||
includeIn: 'read' as 'read',
|
||||
alerting: {
|
||||
alert: {
|
||||
read: Object.values(AlertType),
|
||||
},
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface Feature {
|
||||
|
|
|
@ -125,6 +125,7 @@ export function mergeConfigs(
|
|||
export const plugin = (initContext: PluginInitializerContext) =>
|
||||
new APMPlugin(initContext);
|
||||
|
||||
export { APM_SERVER_FEATURE_ID } from '../common/alert_types';
|
||||
export { APMPlugin } from './plugin';
|
||||
export { APMPluginSetup } from './types';
|
||||
export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository';
|
||||
|
|
|
@ -17,7 +17,11 @@ import {
|
|||
getEnvironmentEsField,
|
||||
getEnvironmentLabel,
|
||||
} from '../../../common/environment_filter_values';
|
||||
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
|
||||
import {
|
||||
AlertType,
|
||||
APM_SERVER_FEATURE_ID,
|
||||
ALERT_TYPES_CONFIG,
|
||||
} from '../../../common/alert_types';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_ENVIRONMENT,
|
||||
|
@ -69,7 +73,7 @@ export function registerErrorCountAlertType({
|
|||
apmActionVariables.interval,
|
||||
],
|
||||
},
|
||||
producer: 'apm',
|
||||
producer: APM_SERVER_FEATURE_ID,
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
executor: async ({ services, params }) => {
|
||||
|
|
|
@ -17,7 +17,11 @@ import {
|
|||
getEnvironmentLabel,
|
||||
getEnvironmentEsField,
|
||||
} from '../../../common/environment_filter_values';
|
||||
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
|
||||
import {
|
||||
AlertType,
|
||||
APM_SERVER_FEATURE_ID,
|
||||
ALERT_TYPES_CONFIG,
|
||||
} from '../../../common/alert_types';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME,
|
||||
|
@ -77,7 +81,7 @@ export function registerTransactionDurationAlertType({
|
|||
apmActionVariables.interval,
|
||||
],
|
||||
},
|
||||
producer: 'apm',
|
||||
producer: APM_SERVER_FEATURE_ID,
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
executor: async ({ services, params }) => {
|
||||
|
|
|
@ -17,7 +17,11 @@ import {
|
|||
getEnvironmentLabel,
|
||||
} from '../../../common/environment_filter_values';
|
||||
import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server';
|
||||
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
|
||||
import {
|
||||
AlertType,
|
||||
ALERT_TYPES_CONFIG,
|
||||
APM_SERVER_FEATURE_ID,
|
||||
} from '../../../common/alert_types';
|
||||
import {
|
||||
EVENT_OUTCOME,
|
||||
PROCESSOR_EVENT,
|
||||
|
@ -75,7 +79,7 @@ export function registerTransactionErrorRateAlertType({
|
|||
apmActionVariables.interval,
|
||||
],
|
||||
},
|
||||
producer: 'apm',
|
||||
producer: APM_SERVER_FEATURE_ID,
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
executor: async ({ services, params: alertParams }) => {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { of } from 'rxjs';
|
|||
import { elasticsearchServiceMock } from 'src/core/server/mocks';
|
||||
import type { RuleDataClient } from '../../../../../rule_registry/server';
|
||||
import { PluginSetupContract as AlertingPluginSetupContract } from '../../../../../alerting/server';
|
||||
import { APMConfig } from '../../..';
|
||||
import { APMConfig, APM_SERVER_FEATURE_ID } from '../../..';
|
||||
|
||||
export const createRuleTypeMocks = () => {
|
||||
let alertExecutor: (...args: any[]) => Promise<any>;
|
||||
|
@ -38,6 +38,9 @@ export const createRuleTypeMocks = () => {
|
|||
|
||||
const services = {
|
||||
scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
savedObjectsClient: {
|
||||
get: () => ({ attributes: { consumer: APM_SERVER_FEATURE_ID } }),
|
||||
},
|
||||
alertInstanceFactory: jest.fn(() => ({ scheduleActions })),
|
||||
alertWithLifecycle: jest.fn(),
|
||||
logger: loggerMock,
|
||||
|
@ -67,6 +70,7 @@ export const createRuleTypeMocks = () => {
|
|||
executor: async ({ params }: { params: Record<string, any> }) => {
|
||||
return alertExecutor({
|
||||
services,
|
||||
rule: { consumer: APM_SERVER_FEATURE_ID },
|
||||
params,
|
||||
startedAt: new Date(),
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
import { mapValues, once } from 'lodash';
|
||||
import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets';
|
||||
import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map';
|
||||
import { APMConfig, APMXPackConfig } from '.';
|
||||
import { APMConfig, APMXPackConfig, APM_SERVER_FEATURE_ID } from '.';
|
||||
import { mergeConfigs } from './index';
|
||||
import { UI_SETTINGS } from '../../../../src/plugins/data/common';
|
||||
import { APM_FEATURE, registerFeaturesUsage } from './feature';
|
||||
|
@ -188,6 +188,7 @@ export class APMPlugin
|
|||
);
|
||||
|
||||
const ruleDataClient = ruleDataService.getRuleDataClient(
|
||||
APM_SERVER_FEATURE_ID,
|
||||
ruleDataService.getFullAssetName('observability-apm'),
|
||||
() => initializeRuleDataTemplatesPromise
|
||||
);
|
||||
|
@ -206,7 +207,7 @@ export class APMPlugin
|
|||
}) as APMRouteHandlerResources['plugins'];
|
||||
|
||||
const telemetryUsageCounter = resourcePlugins.usageCollection?.setup.createUsageCounter(
|
||||
'apm'
|
||||
APM_SERVER_FEATURE_ID
|
||||
);
|
||||
|
||||
registerRoutes({
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from 'src/core/server';
|
||||
import { RuleDataClient } from '../../../rule_registry/server';
|
||||
import { AlertingApiRequestHandlerContext } from '../../../alerting/server';
|
||||
import type { RacApiRequestHandlerContext } from '../../../rule_registry/server';
|
||||
import { LicensingApiRequestHandlerContext } from '../../../licensing/server';
|
||||
import { APMConfig } from '..';
|
||||
import { APMPluginDependencies } from '../types';
|
||||
|
@ -21,6 +22,7 @@ import { APMPluginDependencies } from '../types';
|
|||
export interface ApmPluginRequestHandlerContext extends RequestHandlerContext {
|
||||
licensing: LicensingApiRequestHandlerContext;
|
||||
alerting: AlertingApiRequestHandlerContext;
|
||||
rac: RacApiRequestHandlerContext;
|
||||
}
|
||||
|
||||
export type InspectResponse = Array<{
|
||||
|
|
|
@ -15,9 +15,9 @@ import {
|
|||
SerializerOrUndefined,
|
||||
Type,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { encodeHitVersion } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { transformListItemToElasticQuery } from '../utils';
|
||||
import { encodeHitVersion } from '../utils/encode_hit_version';
|
||||
import { IndexEsListItemSchema } from '../../schemas/elastic_query';
|
||||
|
||||
export interface CreateListItemOptions {
|
||||
|
|
|
@ -12,10 +12,9 @@ import type {
|
|||
MetaOrUndefined,
|
||||
_VersionOrUndefined,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { transformListItemToElasticQuery } from '../utils';
|
||||
import { decodeVersion } from '../utils/decode_version';
|
||||
import { encodeHitVersion } from '../utils/encode_hit_version';
|
||||
import { UpdateEsListItemSchema } from '../../schemas/elastic_query';
|
||||
|
||||
import { getListItem } from './get_list_item';
|
||||
|
|
|
@ -19,8 +19,8 @@ import type {
|
|||
Type,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { Version } from '@kbn/securitysolution-io-ts-types';
|
||||
import { encodeHitVersion } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { encodeHitVersion } from '../utils/encode_hit_version';
|
||||
import { IndexEsListSchema } from '../../schemas/elastic_query';
|
||||
|
||||
export interface CreateListOptions {
|
||||
|
|
|
@ -15,9 +15,8 @@ import type {
|
|||
_VersionOrUndefined,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { VersionOrUndefined } from '@kbn/securitysolution-io-ts-types';
|
||||
import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { decodeVersion } from '../utils/decode_version';
|
||||
import { encodeHitVersion } from '../utils/encode_hit_version';
|
||||
import { UpdateEsListSchema } from '../../schemas/elastic_query';
|
||||
|
||||
import { getList } from '.';
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
*/
|
||||
|
||||
export * from './calculate_scroll_math';
|
||||
export * from './decode_version';
|
||||
export * from './encode_decode_cursor';
|
||||
export * from './encode_hit_version';
|
||||
export * from './escape_query';
|
||||
export * from './find_source_type';
|
||||
export * from './find_source_value';
|
||||
|
|
|
@ -7,11 +7,10 @@
|
|||
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import type { ListArraySchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { encodeHitVersion } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { SearchEsListSchema } from '../../schemas/elastic_response';
|
||||
|
||||
import { encodeHitVersion } from './encode_hit_version';
|
||||
|
||||
export interface TransformElasticToListOptions {
|
||||
response: estypes.SearchResponse<SearchEsListSchema>;
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import type { ListItemArraySchema, Type } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { encodeHitVersion } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { ErrorWithStatusCode } from '../../error_with_status_code';
|
||||
import { SearchEsListItemSchema } from '../../schemas/elastic_response';
|
||||
|
||||
import { encodeHitVersion } from './encode_hit_version';
|
||||
import { findSourceValue } from './find_source_value';
|
||||
|
||||
export interface TransformElasticToListItemOptions {
|
||||
|
|
|
@ -20,6 +20,7 @@ import type {
|
|||
ActionsApiRequestHandlerContext,
|
||||
} from '../../actions/server';
|
||||
import type { AlertingApiRequestHandlerContext } from '../../alerting/server';
|
||||
import type { RacApiRequestHandlerContext } from '../../rule_registry/server';
|
||||
import {
|
||||
PluginStartContract as AlertingPluginStartContract,
|
||||
PluginSetupContract as AlertingPluginSetupContract,
|
||||
|
@ -57,6 +58,7 @@ export interface RequestHandlerContextMonitoringPlugin extends RequestHandlerCon
|
|||
actions?: ActionsApiRequestHandlerContext;
|
||||
alerting?: AlertingApiRequestHandlerContext;
|
||||
infra: InfraRequestHandlerContext;
|
||||
ruleRegistry?: RacApiRequestHandlerContext;
|
||||
}
|
||||
|
||||
export interface PluginsStart {
|
||||
|
|
|
@ -99,6 +99,7 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
const start = () => core.getStartServices().then(([coreStart]) => coreStart);
|
||||
|
||||
const ruleDataClient = plugins.ruleRegistry.ruleDataService.getRuleDataClient(
|
||||
'observability',
|
||||
plugins.ruleRegistry.ruleDataService.getFullAssetName(),
|
||||
() => Promise.resolve()
|
||||
);
|
||||
|
|
|
@ -27,9 +27,7 @@ On plugin setup, rule type producers can create the index template as follows:
|
|||
```ts
|
||||
// get the FQN of the component template. All assets are prefixed with the configured `index` value, which is `.alerts` by default.
|
||||
|
||||
const componentTemplateName = plugins.ruleRegistry.getFullAssetName(
|
||||
'apm-mappings'
|
||||
);
|
||||
const componentTemplateName = plugins.ruleRegistry.getFullAssetName('apm-mappings');
|
||||
|
||||
// if write is disabled, don't install these templates
|
||||
if (!plugins.ruleRegistry.isWriteEnabled()) {
|
||||
|
@ -73,14 +71,10 @@ await plugins.ruleRegistry.createOrUpdateComponentTemplate({
|
|||
await plugins.ruleRegistry.createOrUpdateIndexTemplate({
|
||||
name: plugins.ruleRegistry.getFullAssetName('apm-index-template'),
|
||||
body: {
|
||||
index_patterns: [
|
||||
plugins.ruleRegistry.getFullAssetName('observability-apm*'),
|
||||
],
|
||||
index_patterns: [plugins.ruleRegistry.getFullAssetName('observability-apm*')],
|
||||
composed_of: [
|
||||
// Technical component template, required
|
||||
plugins.ruleRegistry.getFullAssetName(
|
||||
TECHNICAL_COMPONENT_TEMPLATE_NAME
|
||||
),
|
||||
plugins.ruleRegistry.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME),
|
||||
componentTemplateName,
|
||||
],
|
||||
},
|
||||
|
@ -107,8 +101,7 @@ await ruleDataClient.getWriter().bulk({
|
|||
// to read data, simply call ruleDataClient.getReader().search:
|
||||
const response = await ruleDataClient.getReader().search({
|
||||
body: {
|
||||
query: {
|
||||
},
|
||||
query: {},
|
||||
size: 100,
|
||||
fields: ['*'],
|
||||
sort: {
|
||||
|
@ -132,6 +125,7 @@ The following fields are defined in the technical field component template and s
|
|||
- `rule.name`: the name of the rule (as specified by the user).
|
||||
- `rule.category`: the name of the rule type (as defined by the rule type producer)
|
||||
- `kibana.rac.alert.producer`: the producer of the rule type. Usually a Kibana plugin. e.g., `APM`.
|
||||
- `kibana.rac.alert.owner`: the feature which produced the alert. Usually a Kibana feature id like `apm`, `siem`...
|
||||
- `kibana.rac.alert.id`: the id of the alert, that is unique within the context of the rule execution it was created in. E.g., for a rule that monitors latency for all services in all environments, this might be `opbeans-java:production`.
|
||||
- `kibana.rac.alert.uuid`: the unique identifier for the alert during its lifespan. If an alert recovers (or closes), this identifier is re-generated when it is opened again.
|
||||
- `kibana.rac.alert.status`: the status of the alert. Can be `open` or `closed`.
|
||||
|
@ -145,3 +139,16 @@ The following fields are defined in the technical field component template and s
|
|||
- `kibana.rac.alert.ancestors`: the array of ancestors (if any) for the alert.
|
||||
- `kibana.rac.alert.depth`: the depth of the alert in the ancestral tree (default 0).
|
||||
- `kibana.rac.alert.building_block_type`: the building block type of the alert (default undefined).
|
||||
|
||||
# Alerts as data
|
||||
|
||||
Alerts as data can be interacted with using the AlertsClient api found in `x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts`
|
||||
|
||||
This api includes public methods such as
|
||||
|
||||
[x] getFullAssetName
|
||||
[x] getAlertsIndex
|
||||
[x] get
|
||||
[x] update
|
||||
[ ] bulkUpdate (TODO)
|
||||
[ ] find (TODO)
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
ALERT_UUID,
|
||||
EVENT_ACTION,
|
||||
EVENT_KIND,
|
||||
OWNER,
|
||||
PRODUCER,
|
||||
RULE_CATEGORY,
|
||||
RULE_ID,
|
||||
|
@ -40,6 +41,7 @@ export const technicalRuleFieldMap = {
|
|||
RULE_CATEGORY,
|
||||
TAGS
|
||||
),
|
||||
[OWNER]: { type: 'keyword' },
|
||||
[PRODUCER]: { type: 'keyword' },
|
||||
[ALERT_UUID]: { type: 'keyword' },
|
||||
[ALERT_ID]: { type: 'keyword' },
|
||||
|
|
8
x-pack/plugins/rule_registry/common/constants.ts
Normal file
8
x-pack/plugins/rule_registry/common/constants.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const BASE_RAC_ALERTS_API_PATH = '/internal/rac/alerts';
|
44
x-pack/plugins/rule_registry/docs/README.md
Normal file
44
x-pack/plugins/rule_registry/docs/README.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Alerts as data Client API Docs
|
||||
|
||||
This directory contains generated docs using `typedoc` for the alerts as data client (alerts client) API that can be called from other server
|
||||
plugins. This README will describe how to generate a new version of these markdown docs in the event that new methods
|
||||
or parameters are added.
|
||||
|
||||
## TypeDoc Info
|
||||
|
||||
See more info at: <https://typedoc.org/>
|
||||
and: <https://www.npmjs.com/package/typedoc-plugin-markdown> for the markdown plugin
|
||||
|
||||
## Install dependencies
|
||||
|
||||
```bash
|
||||
yarn global add typedoc typedoc-plugin-markdown
|
||||
```
|
||||
|
||||
## Generate the docs
|
||||
|
||||
```bash
|
||||
cd x-pack/plugins/rule_registry/docs
|
||||
npx typedoc --options alerts_client_typedoc.json
|
||||
```
|
||||
|
||||
After running the above commands the files in the `server` directory will be updated to match the new tsdocs.
|
||||
If additional markdown directory should be created we can create a new typedoc configuration file and adjust the `out`
|
||||
directory accordingly.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
This will use the global `tsc` so ensure typescript is installed globally and one of typescript version `3.9, 4.0, 4.1, 4.2`.
|
||||
|
||||
```
|
||||
$ tsc --version
|
||||
Version 4.2.4
|
||||
```
|
||||
|
||||
If you run into tsc errors that seem unrelated to the cases plugin try executing these commands before running `typedoc`
|
||||
|
||||
```bash
|
||||
cd <kibana root dir>
|
||||
npx yarn kbn bootstrap
|
||||
node scripts/build_ts_refs.js --clean --no-cache
|
||||
```
|
|
@ -0,0 +1,14 @@
|
|||
Alerts as data client API Interface
|
||||
|
||||
# Alerts as data client API Interface
|
||||
|
||||
## Table of contents
|
||||
|
||||
### Classes
|
||||
|
||||
- [AlertsClient](classes/alertsclient.md)
|
||||
|
||||
### Interfaces
|
||||
|
||||
- [ConstructorOptions](interfaces/constructoroptions.md)
|
||||
- [UpdateOptions](interfaces/updateoptions.md)
|
|
@ -0,0 +1,191 @@
|
|||
[Alerts as data client API Interface](../alerts_client_api.md) / AlertsClient
|
||||
|
||||
# Class: AlertsClient
|
||||
|
||||
Provides apis to interact with alerts as data
|
||||
ensures the request is authorized to perform read / write actions
|
||||
on alerts as data.
|
||||
|
||||
## Table of contents
|
||||
|
||||
### Constructors
|
||||
|
||||
- [constructor](alertsclient.md#constructor)
|
||||
|
||||
### Properties
|
||||
|
||||
- [auditLogger](alertsclient.md#auditlogger)
|
||||
- [authorization](alertsclient.md#authorization)
|
||||
- [esClient](alertsclient.md#esclient)
|
||||
- [logger](alertsclient.md#logger)
|
||||
|
||||
### Methods
|
||||
|
||||
- [fetchAlert](alertsclient.md#fetchalert)
|
||||
- [get](alertsclient.md#get)
|
||||
- [getAlertsIndex](alertsclient.md#getalertsindex)
|
||||
- [getAuthorizedAlertsIndices](alertsclient.md#getauthorizedalertsindices)
|
||||
- [update](alertsclient.md#update)
|
||||
|
||||
## Constructors
|
||||
|
||||
### constructor
|
||||
|
||||
• **new AlertsClient**(`__namedParameters`)
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `__namedParameters` | [ConstructorOptions](../interfaces/constructoroptions.md) |
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:59](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59)
|
||||
|
||||
## Properties
|
||||
|
||||
### auditLogger
|
||||
|
||||
• `Private` `Optional` `Readonly` **auditLogger**: `AuditLogger`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:57](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L57)
|
||||
|
||||
___
|
||||
|
||||
### authorization
|
||||
|
||||
• `Private` `Readonly` **authorization**: `PublicMethodsOf`<AlertingAuthorization\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:58](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L58)
|
||||
|
||||
___
|
||||
|
||||
### esClient
|
||||
|
||||
• `Private` `Readonly` **esClient**: `ElasticsearchClient`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:59](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59)
|
||||
|
||||
___
|
||||
|
||||
### logger
|
||||
|
||||
• `Private` `Readonly` **logger**: `Logger`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:56](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L56)
|
||||
|
||||
## Methods
|
||||
|
||||
### fetchAlert
|
||||
|
||||
▸ `Private` **fetchAlert**(`__namedParameters`): `Promise`<AlertType\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `__namedParameters` | `GetAlertParams` |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<AlertType\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:79](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L79)
|
||||
|
||||
___
|
||||
|
||||
### get
|
||||
|
||||
▸ **get**(`__namedParameters`): `Promise`<OutputOf<SetOptional<`Object`\>\>\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `__namedParameters` | `GetAlertParams` |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<OutputOf<SetOptional<`Object`\>\>\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:108](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L108)
|
||||
|
||||
___
|
||||
|
||||
### getAlertsIndex
|
||||
|
||||
▸ **getAlertsIndex**(`featureIds`, `operations`): `Promise`<`Object`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `featureIds` | `string`[] |
|
||||
| `operations` | (`ReadOperations` \| `WriteOperations`)[] |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`Object`\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:68](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L68)
|
||||
|
||||
___
|
||||
|
||||
### getAuthorizedAlertsIndices
|
||||
|
||||
▸ **getAuthorizedAlertsIndices**(`featureIds`): `Promise`<undefined \| string[]\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `featureIds` | `string`[] |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<undefined \| string[]\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:200](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L200)
|
||||
|
||||
___
|
||||
|
||||
### update
|
||||
|
||||
▸ **update**<Params\>(`__namedParameters`): `Promise`<`Object`\>
|
||||
|
||||
#### Type parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `Params` | `Params`: `AlertTypeParams` = `never` |
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `__namedParameters` | [UpdateOptions](../interfaces/updateoptions.md)<Params\> |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`Object`\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:146](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L146)
|
|
@ -0,0 +1,52 @@
|
|||
[Alerts as data client API Interface](../alerts_client_api.md) / ConstructorOptions
|
||||
|
||||
# Interface: ConstructorOptions
|
||||
|
||||
## Table of contents
|
||||
|
||||
### Properties
|
||||
|
||||
- [auditLogger](constructoroptions.md#auditlogger)
|
||||
- [authorization](constructoroptions.md#authorization)
|
||||
- [esClient](constructoroptions.md#esclient)
|
||||
- [logger](constructoroptions.md#logger)
|
||||
|
||||
## Properties
|
||||
|
||||
### auditLogger
|
||||
|
||||
• `Optional` **auditLogger**: `AuditLogger`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:34](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L34)
|
||||
|
||||
___
|
||||
|
||||
### authorization
|
||||
|
||||
• **authorization**: `PublicMethodsOf`<AlertingAuthorization\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:33](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L33)
|
||||
|
||||
___
|
||||
|
||||
### esClient
|
||||
|
||||
• **esClient**: `ElasticsearchClient`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:35](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L35)
|
||||
|
||||
___
|
||||
|
||||
### logger
|
||||
|
||||
• **logger**: `Logger`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:32](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L32)
|
|
@ -0,0 +1,58 @@
|
|||
[Alerts as data client API Interface](../alerts_client_api.md) / UpdateOptions
|
||||
|
||||
# Interface: UpdateOptions<Params\>
|
||||
|
||||
## Type parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `Params` | `Params`: `AlertTypeParams` |
|
||||
|
||||
## Table of contents
|
||||
|
||||
### Properties
|
||||
|
||||
- [\_version](updateoptions.md#_version)
|
||||
- [id](updateoptions.md#id)
|
||||
- [index](updateoptions.md#index)
|
||||
- [status](updateoptions.md#status)
|
||||
|
||||
## Properties
|
||||
|
||||
### \_version
|
||||
|
||||
• **\_version**: `undefined` \| `string`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:41](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L41)
|
||||
|
||||
___
|
||||
|
||||
### id
|
||||
|
||||
• **id**: `string`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:39](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L39)
|
||||
|
||||
___
|
||||
|
||||
### index
|
||||
|
||||
• **index**: `string`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:42](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L42)
|
||||
|
||||
___
|
||||
|
||||
### status
|
||||
|
||||
• **status**: `string`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:40](https://github.com/dhurley14/kibana/blob/d2173f5090e/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L40)
|
17
x-pack/plugins/rule_registry/docs/alerts_client_typedoc.json
Normal file
17
x-pack/plugins/rule_registry/docs/alerts_client_typedoc.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"entryPoints": [
|
||||
"../server/alert_data_client/alerts_client.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/mock.ts",
|
||||
"../server/alert_data_client/+(mock.ts|utils.ts|utils.test.ts|types.ts)"
|
||||
],
|
||||
"excludeExternals": true,
|
||||
"out": "alerts_client",
|
||||
"theme": "markdown",
|
||||
"plugin": "typedoc-plugin-markdown",
|
||||
"entryDocument": "alerts_client_api.md",
|
||||
"readme": "none",
|
||||
"name": "Alerts as data client API Interface"
|
||||
}
|
||||
|
|
@ -12,5 +12,6 @@
|
|||
"spaces",
|
||||
"triggersActionsUi"
|
||||
],
|
||||
"optionalPlugins": ["security"],
|
||||
"server": true
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { AlertsClient } from './alerts_client';
|
||||
|
||||
type Schema = PublicMethodsOf<AlertsClient>;
|
||||
export type AlertsClientMock = jest.Mocked<Schema>;
|
||||
|
||||
const createAlertsClientMock = () => {
|
||||
const mocked: AlertsClientMock = {
|
||||
get: jest.fn(),
|
||||
getAlertsIndex: jest.fn(),
|
||||
update: jest.fn(),
|
||||
getAuthorizedAlertsIndices: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const alertsClientMock: {
|
||||
create: () => AlertsClientMock;
|
||||
} = {
|
||||
create: createAlertsClientMock,
|
||||
};
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* 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 { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils';
|
||||
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';
|
||||
import { mapConsumerToIndexName, validFeatureIds, isValidFeatureId } from '../utils/rbac';
|
||||
|
||||
// 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;
|
||||
status: string;
|
||||
_version: string | undefined;
|
||||
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[],
|
||||
operations: Array<ReadOperations | WriteOperations>
|
||||
) {
|
||||
return this.authorization.getAugmentedRuleTypesWithAuthorization(
|
||||
featureIds.length !== 0 ? featureIds : validFeatureIds,
|
||||
operations,
|
||||
AlertingAuthorizationEntity.Alert
|
||||
);
|
||||
}
|
||||
|
||||
private async fetchAlert({
|
||||
id,
|
||||
index,
|
||||
}: GetAlertParams): Promise<(AlertType & { _version: string | undefined }) | null | undefined> {
|
||||
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-*',
|
||||
ignore_unavailable: true,
|
||||
body: { query: { term: { _id: id } } },
|
||||
seq_no_primary_term: true,
|
||||
});
|
||||
|
||||
if (result == null || result.body == null || result.body.hits.hits.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
_version: encodeHitVersion(result.body.hits.hits[0]),
|
||||
};
|
||||
} 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 | null | undefined> {
|
||||
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,
|
||||
});
|
||||
|
||||
if (alert == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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,
|
||||
status,
|
||||
_version,
|
||||
index,
|
||||
}: UpdateOptions<Params>) {
|
||||
try {
|
||||
const alert = await this.fetchAlert({
|
||||
id,
|
||||
index,
|
||||
});
|
||||
|
||||
if (alert == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.authorization.ensureAuthorized({
|
||||
ruleTypeId: alert[RULE_ID],
|
||||
consumer: alert[OWNER],
|
||||
operation: WriteOperations.Update,
|
||||
entity: AlertingAuthorizationEntity.Alert,
|
||||
});
|
||||
|
||||
this.auditLogger?.log(
|
||||
alertAuditEvent({
|
||||
action: AlertAuditAction.UPDATE,
|
||||
id,
|
||||
outcome: 'unknown',
|
||||
})
|
||||
);
|
||||
|
||||
const { body: response } = await this.esClient.update<ParsedTechnicalFields>({
|
||||
...decodeVersion(_version),
|
||||
id,
|
||||
index,
|
||||
body: {
|
||||
doc: {
|
||||
[ALERT_STATUS]: status,
|
||||
},
|
||||
},
|
||||
refresh: 'wait_for',
|
||||
});
|
||||
|
||||
return {
|
||||
...response,
|
||||
_version: encodeHitVersion(response),
|
||||
};
|
||||
} 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.getAugmentedRuleTypesWithAuthorization(
|
||||
featureIds,
|
||||
[ReadOperations.Find, ReadOperations.Get, WriteOperations.Update],
|
||||
AlertingAuthorizationEntity.Alert
|
||||
);
|
||||
|
||||
// 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 = new Set<string>();
|
||||
for (const ruleType of augmentedRuleTypes.authorizedRuleTypes) {
|
||||
authorizedFeatures.add(ruleType.producer);
|
||||
}
|
||||
|
||||
const toReturn = Array.from(authorizedFeatures).flatMap((feature) => {
|
||||
if (isValidFeatureId(feature)) {
|
||||
return mapConsumerToIndexName[feature];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { Request } from '@hapi/hapi';
|
||||
|
||||
import { AlertsClientFactory, AlertsClientFactoryProps } from './alerts_client_factory';
|
||||
import { ElasticsearchClient, KibanaRequest } from 'src/core/server';
|
||||
import { loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { securityMock } from '../../../security/server/mocks';
|
||||
import { AuditLogger } from '../../../security/server';
|
||||
import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock';
|
||||
|
||||
jest.mock('./alerts_client');
|
||||
|
||||
const securityPluginSetup = securityMock.createSetup();
|
||||
const alertingAuthMock = alertingAuthorizationMock.create();
|
||||
|
||||
const alertsClientFactoryParams: AlertsClientFactoryProps = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
getAlertingAuthorization: (_: KibanaRequest) => alertingAuthMock,
|
||||
securityPluginSetup,
|
||||
esClient: {} as ElasticsearchClient,
|
||||
};
|
||||
|
||||
const fakeRequest = ({
|
||||
app: {},
|
||||
headers: {},
|
||||
getBasePath: () => '',
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
href: '/',
|
||||
},
|
||||
raw: {
|
||||
req: {
|
||||
url: '/',
|
||||
},
|
||||
},
|
||||
} as unknown) as Request;
|
||||
|
||||
const auditLogger = {
|
||||
log: jest.fn(),
|
||||
} as jest.Mocked<AuditLogger>;
|
||||
|
||||
describe('AlertsClientFactory', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
securityPluginSetup.audit.asScoped.mockReturnValue(auditLogger);
|
||||
});
|
||||
|
||||
test('creates an alerts client with proper constructor arguments', async () => {
|
||||
const factory = new AlertsClientFactory();
|
||||
factory.initialize({ ...alertsClientFactoryParams });
|
||||
const request = KibanaRequest.from(fakeRequest);
|
||||
await factory.create(request);
|
||||
|
||||
expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({
|
||||
authorization: alertingAuthMock,
|
||||
logger: alertsClientFactoryParams.logger,
|
||||
auditLogger,
|
||||
esClient: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('throws an error if already initialized', () => {
|
||||
const factory = new AlertsClientFactory();
|
||||
factory.initialize({ ...alertsClientFactoryParams });
|
||||
|
||||
expect(() =>
|
||||
factory.initialize({ ...alertsClientFactoryParams })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"AlertsClientFactory (RAC) already initialized"`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient, KibanaRequest, Logger } from 'src/core/server';
|
||||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { SecurityPluginSetup } from '../../../security/server';
|
||||
import { AlertingAuthorization } from '../../../alerting/server';
|
||||
import { AlertsClient } from './alerts_client';
|
||||
|
||||
export interface AlertsClientFactoryProps {
|
||||
logger: Logger;
|
||||
esClient: ElasticsearchClient;
|
||||
getAlertingAuthorization: (request: KibanaRequest) => PublicMethodsOf<AlertingAuthorization>;
|
||||
securityPluginSetup: SecurityPluginSetup | undefined;
|
||||
}
|
||||
|
||||
export class AlertsClientFactory {
|
||||
private isInitialized = false;
|
||||
private logger!: Logger;
|
||||
private esClient!: ElasticsearchClient;
|
||||
private getAlertingAuthorization!: (
|
||||
request: KibanaRequest
|
||||
) => PublicMethodsOf<AlertingAuthorization>;
|
||||
private securityPluginSetup!: SecurityPluginSetup | undefined;
|
||||
|
||||
public initialize(options: AlertsClientFactoryProps) {
|
||||
/**
|
||||
* This should be called by the plugin's start() method.
|
||||
*/
|
||||
if (this.isInitialized) {
|
||||
throw new Error('AlertsClientFactory (RAC) already initialized');
|
||||
}
|
||||
|
||||
this.getAlertingAuthorization = options.getAlertingAuthorization;
|
||||
this.isInitialized = true;
|
||||
this.logger = options.logger;
|
||||
this.esClient = options.esClient;
|
||||
this.securityPluginSetup = options.securityPluginSetup;
|
||||
}
|
||||
|
||||
public async create(request: KibanaRequest): Promise<AlertsClient> {
|
||||
const { securityPluginSetup, getAlertingAuthorization, logger } = this;
|
||||
|
||||
return new AlertsClient({
|
||||
logger,
|
||||
authorization: getAlertingAuthorization(request),
|
||||
auditLogger: securityPluginSetup?.audit.asScoped(request),
|
||||
esClient: this.esClient,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 { AlertAuditAction, alertAuditEvent } from './audit_events';
|
||||
|
||||
describe('#alertAuditEvent', () => {
|
||||
test('creates event with `unknown` outcome', () => {
|
||||
expect(
|
||||
alertAuditEvent({
|
||||
action: AlertAuditAction.GET,
|
||||
outcome: 'unknown',
|
||||
id: '123',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": undefined,
|
||||
"event": Object {
|
||||
"action": "alert_get",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "unknown",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"message": "User is accessing alert [id=123]",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('creates event with `success` outcome', () => {
|
||||
expect(
|
||||
alertAuditEvent({
|
||||
action: AlertAuditAction.GET,
|
||||
id: '123',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": undefined,
|
||||
"event": Object {
|
||||
"action": "alert_get",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "success",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"message": "User has accessed alert [id=123]",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('creates event with `failure` outcome', () => {
|
||||
expect(
|
||||
alertAuditEvent({
|
||||
action: AlertAuditAction.GET,
|
||||
id: '123',
|
||||
error: new Error('ERROR_MESSAGE'),
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "ERROR_MESSAGE",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "alert_get",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"message": "Failed attempt to access alert [id=123]",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { EcsEventOutcome, EcsEventType } from 'src/core/server';
|
||||
import { AuditEvent } from '../../../security/server';
|
||||
|
||||
export enum AlertAuditAction {
|
||||
GET = 'alert_get',
|
||||
UPDATE = 'alert_update',
|
||||
FIND = 'alert_find',
|
||||
}
|
||||
|
||||
type VerbsTuple = [string, string, string];
|
||||
|
||||
const eventVerbs: Record<AlertAuditAction, VerbsTuple> = {
|
||||
alert_get: ['access', 'accessing', 'accessed'],
|
||||
alert_update: ['update', 'updating', 'updated'],
|
||||
alert_find: ['access', 'accessing', 'accessed'],
|
||||
};
|
||||
|
||||
const eventTypes: Record<AlertAuditAction, EcsEventType> = {
|
||||
alert_get: 'access',
|
||||
alert_update: 'change',
|
||||
alert_find: 'access',
|
||||
};
|
||||
|
||||
export interface AlertAuditEventParams {
|
||||
action: AlertAuditAction;
|
||||
outcome?: EcsEventOutcome;
|
||||
id?: string;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export function alertAuditEvent({ action, id, outcome, error }: AlertAuditEventParams): AuditEvent {
|
||||
const doc = id ? `alert [id=${id}]` : 'an alert';
|
||||
const [present, progressive, past] = eventVerbs[action];
|
||||
const message = error
|
||||
? `Failed attempt to ${present} ${doc}`
|
||||
: outcome === 'unknown'
|
||||
? `User is ${progressive} ${doc}`
|
||||
: `User has ${past} ${doc}`;
|
||||
const type = eventTypes[action];
|
||||
|
||||
return {
|
||||
message,
|
||||
event: {
|
||||
action,
|
||||
category: ['database'],
|
||||
type: type ? [type] : undefined,
|
||||
outcome: outcome ?? (error ? 'failure' : 'success'),
|
||||
},
|
||||
error: error && {
|
||||
code: error.name,
|
||||
message: error.message,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
* 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 { AlertsClient, ConstructorOptions } from '../alerts_client';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
|
||||
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
|
||||
import { AuditLogger } from '../../../../security/server';
|
||||
|
||||
const alertingAuthMock = alertingAuthorizationMock.create();
|
||||
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
|
||||
const auditLogger = {
|
||||
log: jest.fn(),
|
||||
} as jest.Mocked<AuditLogger>;
|
||||
|
||||
const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
authorization: alertingAuthMock,
|
||||
esClient: esClientMock,
|
||||
auditLogger,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
test('calls ES client with given params', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
found: true,
|
||||
_type: 'alert',
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_version: 1,
|
||||
_seq_no: 362,
|
||||
_primary_term: 2,
|
||||
_source: {
|
||||
'rule.id': 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
'kibana.rac.alert.owner': 'apm',
|
||||
'kibana.rac.alert.status': 'open',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
const result = await alertsClient.get({ id: '1', index: '.alerts-observability-apm' });
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"_version": "WzM2MiwyXQ==",
|
||||
"kibana.rac.alert.owner": "apm",
|
||||
"kibana.rac.alert.status": "open",
|
||||
"message": "hello world 1",
|
||||
"rule.id": "apm.error_rate",
|
||||
}
|
||||
`);
|
||||
expect(esClientMock.search).toHaveBeenCalledTimes(1);
|
||||
expect(esClientMock.search.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"body": Object {
|
||||
"query": Object {
|
||||
"term": Object {
|
||||
"_id": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"ignore_unavailable": true,
|
||||
"index": ".alerts-observability-apm",
|
||||
"seq_no_primary_term": true,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('logs successful event in audit logger', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
found: true,
|
||||
_type: 'alert',
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_version: 1,
|
||||
_seq_no: 362,
|
||||
_primary_term: 2,
|
||||
_source: {
|
||||
'rule.id': 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
'kibana.rac.alert.owner': 'apm',
|
||||
'kibana.rac.alert.status': 'open',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
await alertsClient.get({ id: '1', index: '.alerts-observability-apm' });
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledWith({
|
||||
error: undefined,
|
||||
event: { action: 'alert_get', category: ['database'], outcome: 'success', type: ['access'] },
|
||||
message: 'User has accessed alert [id=1]',
|
||||
});
|
||||
});
|
||||
|
||||
test(`throws an error if ES client get fails`, async () => {
|
||||
const error = new Error('something went wrong');
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
alertsClient.get({ id: '1', index: '.alerts-observability-apm' })
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong"`);
|
||||
expect(auditLogger.log).toHaveBeenCalledWith({
|
||||
error: { code: 'Error', message: 'something went wrong' },
|
||||
event: { action: 'alert_get', category: ['database'], outcome: 'failure', type: ['access'] },
|
||||
message: 'Failed attempt to access alert [id=1]',
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
beforeEach(() => {
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
found: true,
|
||||
_type: 'alert',
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_version: 1,
|
||||
_seq_no: 362,
|
||||
_primary_term: 2,
|
||||
_source: {
|
||||
'rule.id': 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
'kibana.rac.alert.owner': 'apm',
|
||||
'kibana.rac.alert.status': 'open',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('returns alert if user is authorized to read alert under the consumer', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
const result = await alertsClient.get({ id: '1', index: '.alerts-observability-apm' });
|
||||
|
||||
expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'alert',
|
||||
consumer: 'apm',
|
||||
operation: 'get',
|
||||
ruleTypeId: 'apm.error_rate',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"_version": "WzM2MiwyXQ==",
|
||||
"kibana.rac.alert.owner": "apm",
|
||||
"kibana.rac.alert.status": "open",
|
||||
"message": "hello world 1",
|
||||
"rule.id": "apm.error_rate",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('throws when user is not authorized to get this type of alert', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
alertingAuthMock.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get a "apm.error_rate" alert for "apm"`)
|
||||
);
|
||||
|
||||
await expect(
|
||||
alertsClient.get({ id: '1', index: '.alerts-observability-apm' })
|
||||
).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]`
|
||||
);
|
||||
|
||||
expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'alert',
|
||||
consumer: 'apm',
|
||||
operation: 'get',
|
||||
ruleTypeId: 'apm.error_rate',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,376 @@
|
|||
/*
|
||||
* 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 { AlertsClient, ConstructorOptions } from '../alerts_client';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
|
||||
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
|
||||
import { AuditLogger } from '../../../../security/server';
|
||||
|
||||
const alertingAuthMock = alertingAuthorizationMock.create();
|
||||
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
|
||||
const auditLogger = {
|
||||
log: jest.fn(),
|
||||
} as jest.Mocked<AuditLogger>;
|
||||
|
||||
const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
authorization: alertingAuthMock,
|
||||
esClient: esClientMock,
|
||||
auditLogger,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('update()', () => {
|
||||
test('calls ES client with given params', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
found: true,
|
||||
_type: 'alert',
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_source: {
|
||||
'rule.id': 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
'kibana.rac.alert.owner': 'apm',
|
||||
'kibana.rac.alert.status': 'open',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
esClientMock.update.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_version: 2,
|
||||
result: 'updated',
|
||||
_shards: { total: 2, successful: 1, failed: 0 },
|
||||
_seq_no: 1,
|
||||
_primary_term: 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
const result = await alertsClient.update({
|
||||
id: '1',
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
index: '.alerts-observability-apm',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"_id": "NoxgpHkBqbdrfX07MqXV",
|
||||
"_index": ".alerts-observability-apm",
|
||||
"_primary_term": 1,
|
||||
"_seq_no": 1,
|
||||
"_shards": Object {
|
||||
"failed": 0,
|
||||
"successful": 1,
|
||||
"total": 2,
|
||||
},
|
||||
"_version": "WzEsMV0=",
|
||||
"result": "updated",
|
||||
}
|
||||
`);
|
||||
expect(esClientMock.update).toHaveBeenCalledTimes(1);
|
||||
expect(esClientMock.update.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"body": Object {
|
||||
"doc": Object {
|
||||
"kibana.rac.alert.status": "closed",
|
||||
},
|
||||
},
|
||||
"id": "1",
|
||||
"index": ".alerts-observability-apm",
|
||||
"refresh": "wait_for",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('logs successful event in audit logger', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
found: true,
|
||||
_type: 'alert',
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_source: {
|
||||
'rule.id': 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
'kibana.rac.alert.owner': 'apm',
|
||||
'kibana.rac.alert.status': 'open',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
esClientMock.update.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_version: 2,
|
||||
result: 'updated',
|
||||
_shards: { total: 2, successful: 1, failed: 0 },
|
||||
_seq_no: 1,
|
||||
_primary_term: 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
await alertsClient.update({
|
||||
id: '1',
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
index: '.alerts-observability-apm',
|
||||
});
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledWith({
|
||||
error: undefined,
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'unknown',
|
||||
type: ['change'],
|
||||
},
|
||||
message: 'User is updating alert [id=1]',
|
||||
});
|
||||
});
|
||||
|
||||
test(`throws an error if ES client get fails`, async () => {
|
||||
const error = new Error('something went wrong on get');
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
alertsClient.update({
|
||||
id: '1',
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
index: '.alerts-observability-apm',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong on get"`);
|
||||
expect(auditLogger.log).toHaveBeenCalledWith({
|
||||
error: { code: 'Error', message: 'something went wrong on get' },
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'failure',
|
||||
type: ['change'],
|
||||
},
|
||||
message: 'Failed attempt to update alert [id=1]',
|
||||
});
|
||||
});
|
||||
|
||||
test(`throws an error if ES client update fails`, async () => {
|
||||
const error = new Error('something went wrong on update');
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
found: true,
|
||||
_type: 'alert',
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_source: {
|
||||
'rule.id': 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
'kibana.rac.alert.owner': 'apm',
|
||||
'kibana.rac.alert.status': 'open',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
esClientMock.update.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
alertsClient.update({
|
||||
id: '1',
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
index: '.alerts-observability-apm',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong on update"`);
|
||||
expect(auditLogger.log).toHaveBeenCalledWith({
|
||||
error: { code: 'Error', message: 'something went wrong on update' },
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'failure',
|
||||
type: ['change'],
|
||||
},
|
||||
message: 'Failed attempt to update alert [id=1]',
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
beforeEach(() => {
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
found: true,
|
||||
_type: 'alert',
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_version: 2,
|
||||
_seq_no: 362,
|
||||
_primary_term: 2,
|
||||
_source: {
|
||||
'rule.id': 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
'kibana.rac.alert.owner': 'apm',
|
||||
'kibana.rac.alert.status': 'open',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
esClientMock.update.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_version: 2,
|
||||
result: 'updated',
|
||||
_shards: { total: 2, successful: 1, failed: 0 },
|
||||
_seq_no: 1,
|
||||
_primary_term: 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('returns alert if user is authorized to update alert under the consumer', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
const result = await alertsClient.update({
|
||||
id: '1',
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
index: '.alerts-observability-apm',
|
||||
});
|
||||
|
||||
expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'alert',
|
||||
consumer: 'apm',
|
||||
operation: 'update',
|
||||
ruleTypeId: 'apm.error_rate',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"_id": "NoxgpHkBqbdrfX07MqXV",
|
||||
"_index": ".alerts-observability-apm",
|
||||
"_primary_term": 1,
|
||||
"_seq_no": 1,
|
||||
"_shards": Object {
|
||||
"failed": 0,
|
||||
"successful": 1,
|
||||
"total": 2,
|
||||
},
|
||||
"_version": "WzEsMV0=",
|
||||
"result": "updated",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('throws when user is not authorized to update this type of alert', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
alertingAuthMock.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get a "apm.error_rate" alert for "apm"`)
|
||||
);
|
||||
|
||||
await expect(
|
||||
alertsClient.update({
|
||||
id: '1',
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
index: '.alerts-observability-apm',
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]`
|
||||
);
|
||||
|
||||
expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'alert',
|
||||
consumer: 'apm',
|
||||
operation: 'update',
|
||||
ruleTypeId: 'apm.error_rate',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -72,7 +72,7 @@ export class IndexWriter {
|
|||
for (const item of items) {
|
||||
if (item.doc === undefined) continue;
|
||||
|
||||
bulkBody.push({ create: { _index: item.index } });
|
||||
bulkBody.push({ create: { _index: item.index, version: 1 } });
|
||||
bulkBody.push(item.doc);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { RuleRegistryPlugin } from './plugin';
|
|||
|
||||
export * from './config';
|
||||
export type { RuleRegistryPluginSetupContract, RuleRegistryPluginStartContract } from './plugin';
|
||||
export type { RacRequestHandlerContext, RacApiRequestHandlerContext } from './types';
|
||||
export { RuleDataClient } from './rule_data_client';
|
||||
export { IRuleDataClient } from './rule_data_client/types';
|
||||
export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data';
|
||||
|
|
|
@ -4,19 +4,34 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext, Plugin, CoreSetup, Logger } from 'src/core/server';
|
||||
import {
|
||||
PluginInitializerContext,
|
||||
Plugin,
|
||||
CoreSetup,
|
||||
Logger,
|
||||
KibanaRequest,
|
||||
CoreStart,
|
||||
IContextProvider,
|
||||
} from 'src/core/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
import { AlertsClientFactory } from './alert_data_client/alerts_client_factory';
|
||||
import { PluginStartContract as AlertingStart } from '../../alerting/server';
|
||||
import { RacApiRequestHandlerContext, RacRequestHandlerContext } from './types';
|
||||
import { defineRoutes } from './routes';
|
||||
import { SpacesPluginStart } from '../../spaces/server';
|
||||
|
||||
import { RuleRegistryPluginConfig } from './config';
|
||||
import { RuleDataPluginService } from './rule_data_plugin_service';
|
||||
import { EventLogService, IEventLogService } from './event_log';
|
||||
import { AlertsClient } from './alert_data_client/alerts_client';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface RuleRegistryPluginSetupDependencies {}
|
||||
export interface RuleRegistryPluginSetupDependencies {
|
||||
security?: SecurityPluginSetup;
|
||||
}
|
||||
|
||||
interface RuleRegistryPluginStartDependencies {
|
||||
export interface RuleRegistryPluginStartDependencies {
|
||||
spaces: SpacesPluginStart;
|
||||
alerting: AlertingStart;
|
||||
}
|
||||
|
||||
export interface RuleRegistryPluginSetupContract {
|
||||
|
@ -24,7 +39,10 @@ export interface RuleRegistryPluginSetupContract {
|
|||
eventLogService: IEventLogService;
|
||||
}
|
||||
|
||||
export type RuleRegistryPluginStartContract = void;
|
||||
export interface RuleRegistryPluginStartContract {
|
||||
getRacClientWithRequest: (req: KibanaRequest) => Promise<AlertsClient>;
|
||||
alerting: AlertingStart;
|
||||
}
|
||||
|
||||
export class RuleRegistryPlugin
|
||||
implements
|
||||
|
@ -37,17 +55,23 @@ export class RuleRegistryPlugin
|
|||
private readonly config: RuleRegistryPluginConfig;
|
||||
private readonly logger: Logger;
|
||||
private eventLogService: EventLogService | null;
|
||||
private readonly alertsClientFactory: AlertsClientFactory;
|
||||
private ruleDataService: RuleDataPluginService | null;
|
||||
private security: SecurityPluginSetup | undefined;
|
||||
|
||||
constructor(initContext: PluginInitializerContext) {
|
||||
this.config = initContext.config.get<RuleRegistryPluginConfig>();
|
||||
this.logger = initContext.logger.get();
|
||||
this.eventLogService = null;
|
||||
this.ruleDataService = null;
|
||||
this.alertsClientFactory = new AlertsClientFactory();
|
||||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup<RuleRegistryPluginStartDependencies, RuleRegistryPluginStartContract>
|
||||
core: CoreSetup<RuleRegistryPluginStartDependencies, RuleRegistryPluginStartContract>,
|
||||
plugins: RuleRegistryPluginSetupDependencies
|
||||
): RuleRegistryPluginSetupContract {
|
||||
const { config, logger } = this;
|
||||
const { logger } = this;
|
||||
|
||||
const startDependencies = core.getStartServices().then(([coreStart, pluginStart]) => {
|
||||
return {
|
||||
|
@ -56,23 +80,36 @@ export class RuleRegistryPlugin
|
|||
};
|
||||
});
|
||||
|
||||
const ruleDataService = new RuleDataPluginService({
|
||||
logger,
|
||||
isWriteEnabled: config.write.enabled,
|
||||
index: config.index,
|
||||
this.security = plugins.security;
|
||||
|
||||
const service = new RuleDataPluginService({
|
||||
logger: this.logger,
|
||||
isWriteEnabled: this.config.write.enabled,
|
||||
index: this.config.index,
|
||||
getClusterClient: async () => {
|
||||
const deps = await startDependencies;
|
||||
return deps.core.elasticsearch.client.asInternalUser;
|
||||
},
|
||||
});
|
||||
|
||||
ruleDataService.init().catch((originalError) => {
|
||||
service.init().catch((originalError) => {
|
||||
const error = new Error('Failed installing assets');
|
||||
// @ts-ignore
|
||||
error.stack = originalError.stack;
|
||||
logger.error(error);
|
||||
this.logger.error(error);
|
||||
});
|
||||
|
||||
this.ruleDataService = service;
|
||||
|
||||
// ALERTS ROUTES
|
||||
const router = core.http.createRouter<RacRequestHandlerContext>();
|
||||
core.http.registerRouteHandlerContext<RacRequestHandlerContext, 'rac'>(
|
||||
'rac',
|
||||
this.createRouteHandlerContext()
|
||||
);
|
||||
|
||||
defineRoutes(router);
|
||||
|
||||
const eventLogService = new EventLogService({
|
||||
config: {
|
||||
indexPrefix: this.config.index,
|
||||
|
@ -86,10 +123,47 @@ export class RuleRegistryPlugin
|
|||
});
|
||||
|
||||
this.eventLogService = eventLogService;
|
||||
return { ruleDataService, eventLogService };
|
||||
|
||||
return { ruleDataService: this.ruleDataService, eventLogService };
|
||||
}
|
||||
|
||||
public start(): RuleRegistryPluginStartContract {}
|
||||
public start(
|
||||
core: CoreStart,
|
||||
plugins: RuleRegistryPluginStartDependencies
|
||||
): RuleRegistryPluginStartContract {
|
||||
const { logger, alertsClientFactory, security } = this;
|
||||
|
||||
alertsClientFactory.initialize({
|
||||
logger,
|
||||
esClient: core.elasticsearch.client.asInternalUser,
|
||||
// NOTE: Alerts share the authorization client with the alerting plugin
|
||||
getAlertingAuthorization(request: KibanaRequest) {
|
||||
return plugins.alerting.getAlertingAuthorizationWithRequest(request);
|
||||
},
|
||||
securityPluginSetup: security,
|
||||
});
|
||||
|
||||
const getRacClientWithRequest = (request: KibanaRequest) => {
|
||||
return alertsClientFactory.create(request);
|
||||
};
|
||||
|
||||
return {
|
||||
getRacClientWithRequest,
|
||||
alerting: plugins.alerting,
|
||||
};
|
||||
}
|
||||
|
||||
private createRouteHandlerContext = (): IContextProvider<RacRequestHandlerContext, 'rac'> => {
|
||||
const { alertsClientFactory } = this;
|
||||
return function alertsRouteHandlerContext(context, request): RacApiRequestHandlerContext {
|
||||
return {
|
||||
getAlertsClient: async () => {
|
||||
const createdClient = alertsClientFactory.create(request);
|
||||
return createdClient;
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
public stop() {
|
||||
const { eventLogService, logger } = this;
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { coreMock, elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks';
|
||||
import { alertsClientMock } from '../../alert_data_client/alerts_client.mock';
|
||||
import { RacRequestHandlerContext } from '../../types';
|
||||
|
||||
const createMockClients = () => ({
|
||||
rac: alertsClientMock.create(),
|
||||
clusterClient: elasticsearchServiceMock.createLegacyScopedClusterClient(),
|
||||
newClusterClient: elasticsearchServiceMock.createScopedClusterClient(),
|
||||
savedObjectsClient: savedObjectsClientMock.create(),
|
||||
});
|
||||
|
||||
const createRequestContextMock = (
|
||||
clients: ReturnType<typeof createMockClients> = createMockClients()
|
||||
) => {
|
||||
const coreContext = coreMock.createRequestHandlerContext();
|
||||
return ({
|
||||
rac: { getAlertsClient: jest.fn(() => clients.rac) },
|
||||
core: {
|
||||
...coreContext,
|
||||
elasticsearch: {
|
||||
...coreContext.elasticsearch,
|
||||
client: clients.newClusterClient,
|
||||
legacy: { ...coreContext.elasticsearch.legacy, client: clients.clusterClient },
|
||||
},
|
||||
savedObjects: { client: clients.savedObjectsClient },
|
||||
},
|
||||
} as unknown) as RacRequestHandlerContext;
|
||||
};
|
||||
|
||||
const createTools = () => {
|
||||
const clients = createMockClients();
|
||||
const context = createRequestContextMock(clients);
|
||||
|
||||
return { clients, context };
|
||||
};
|
||||
|
||||
export const requestContextMock = {
|
||||
create: createRequestContextMock,
|
||||
createMockClients,
|
||||
createTools,
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { requestMock } from './server';
|
||||
|
||||
export const getReadRequest = () =>
|
||||
requestMock.create({
|
||||
method: 'get',
|
||||
path: BASE_RAC_ALERTS_API_PATH,
|
||||
query: { id: 'alert-1' },
|
||||
});
|
||||
|
||||
export const getUpdateRequest = () =>
|
||||
requestMock.create({
|
||||
method: 'patch',
|
||||
path: BASE_RAC_ALERTS_API_PATH,
|
||||
body: {
|
||||
status: 'closed',
|
||||
ids: ['alert-1'],
|
||||
index: '.alerts-observability-apm*',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { httpServerMock } from 'src/core/server/mocks';
|
||||
|
||||
const responseMock = {
|
||||
create: httpServerMock.createResponseFactory,
|
||||
};
|
||||
|
||||
type ResponseMock = ReturnType<typeof responseMock.create>;
|
||||
type Method = keyof ResponseMock;
|
||||
|
||||
type MockCall = any;
|
||||
|
||||
interface ResponseCall {
|
||||
body: any;
|
||||
status: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface Response extends ResponseCall {
|
||||
calls: ResponseCall[];
|
||||
}
|
||||
|
||||
const buildResponses = (method: Method, calls: MockCall[]): ResponseCall[] => {
|
||||
if (!calls.length) return [];
|
||||
|
||||
switch (method) {
|
||||
case 'ok':
|
||||
return calls.map(([call]) => ({ status: 200, body: call.body }));
|
||||
case 'customError':
|
||||
return calls.map(([call]) => ({
|
||||
status: call.statusCode,
|
||||
body: call.body,
|
||||
}));
|
||||
default:
|
||||
throw new Error(`Encountered unexpected call to response.${method}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const responseAdapter = (response: ResponseMock): Response => {
|
||||
const methods = Object.keys(response) as Method[];
|
||||
const calls = methods
|
||||
.reduce<Response['calls']>((responses, method) => {
|
||||
const methodMock = response[method];
|
||||
return [...responses, ...buildResponses(method, methodMock.mock.calls)];
|
||||
}, [])
|
||||
.sort((call, other) => other.status - call.status);
|
||||
|
||||
const [{ body, status }] = calls;
|
||||
|
||||
return {
|
||||
body,
|
||||
status,
|
||||
calls,
|
||||
};
|
||||
};
|
103
x-pack/plugins/rule_registry/server/routes/__mocks__/server.ts
Normal file
103
x-pack/plugins/rule_registry/server/routes/__mocks__/server.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { RequestHandler, RouteConfig, KibanaRequest } from 'src/core/server';
|
||||
import { httpServerMock, httpServiceMock } from 'src/core/server/mocks';
|
||||
import { RacRequestHandlerContext } from '../../types';
|
||||
import { requestContextMock } from './request_context';
|
||||
import { responseAdapter } from './response_adapters';
|
||||
|
||||
export const requestMock = {
|
||||
create: httpServerMock.createKibanaRequest,
|
||||
};
|
||||
|
||||
export const responseFactoryMock = {
|
||||
create: httpServerMock.createResponseFactory,
|
||||
};
|
||||
|
||||
interface Route {
|
||||
config: RouteConfig<unknown, unknown, unknown, 'get' | 'post' | 'delete' | 'patch' | 'put'>;
|
||||
handler: RequestHandler;
|
||||
}
|
||||
const getRoute = (routerMock: MockServer['router']): Route => {
|
||||
const routeCalls = [
|
||||
...routerMock.get.mock.calls,
|
||||
...routerMock.post.mock.calls,
|
||||
...routerMock.put.mock.calls,
|
||||
...routerMock.patch.mock.calls,
|
||||
...routerMock.delete.mock.calls,
|
||||
];
|
||||
|
||||
const [route] = routeCalls;
|
||||
if (!route) {
|
||||
throw new Error('No route registered!');
|
||||
}
|
||||
|
||||
const [config, handler] = route;
|
||||
return { config, handler };
|
||||
};
|
||||
|
||||
const buildResultMock = () => ({ ok: jest.fn((x) => x), badRequest: jest.fn((x) => x) });
|
||||
|
||||
class MockServer {
|
||||
constructor(
|
||||
public readonly router = httpServiceMock.createRouter(),
|
||||
private responseMock = responseFactoryMock.create(),
|
||||
private contextMock = requestContextMock.create(),
|
||||
private resultMock = buildResultMock()
|
||||
) {}
|
||||
|
||||
public validate(request: KibanaRequest) {
|
||||
this.validateRequest(request);
|
||||
return this.resultMock;
|
||||
}
|
||||
|
||||
public async inject(
|
||||
request: KibanaRequest,
|
||||
context: RacRequestHandlerContext = this.contextMock
|
||||
) {
|
||||
const validatedRequest = this.validateRequest(request);
|
||||
const [rejection] = this.resultMock.badRequest.mock.calls;
|
||||
if (rejection) {
|
||||
throw new Error(`Request was rejected with message: '${rejection}'`);
|
||||
}
|
||||
|
||||
await this.getRoute().handler(context, validatedRequest, this.responseMock);
|
||||
return responseAdapter(this.responseMock);
|
||||
}
|
||||
|
||||
private getRoute(): Route {
|
||||
return getRoute(this.router);
|
||||
}
|
||||
|
||||
private maybeValidate(part: any, validator?: any): any {
|
||||
return typeof validator === 'function' ? validator(part, this.resultMock) : part;
|
||||
}
|
||||
|
||||
private validateRequest(request: KibanaRequest): KibanaRequest {
|
||||
const validations = this.getRoute().config.validate;
|
||||
if (!validations) {
|
||||
return request;
|
||||
}
|
||||
|
||||
const validatedRequest = requestMock.create({
|
||||
path: request.route.path,
|
||||
method: request.route.method,
|
||||
body: this.maybeValidate(request.body, validations.body),
|
||||
query: this.maybeValidate(request.query, validations.query),
|
||||
params: this.maybeValidate(request.params, validations.params),
|
||||
});
|
||||
|
||||
return validatedRequest;
|
||||
}
|
||||
}
|
||||
|
||||
const createMockServer = () => new MockServer();
|
||||
|
||||
export const serverMock = {
|
||||
create: createMockServer,
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { ParsedTechnicalFields } from '../../common/parse_technical_fields';
|
||||
import { getAlertByIdRoute } from './get_alert_by_id';
|
||||
import { requestContextMock } from './__mocks__/request_context';
|
||||
import { getReadRequest } from './__mocks__/request_responses';
|
||||
import { requestMock, serverMock } from './__mocks__/server';
|
||||
|
||||
const getMockAlert = (): ParsedTechnicalFields => ({
|
||||
'@timestamp': '2021-06-21T21:33:05.713Z',
|
||||
'rule.id': 'apm.error_rate',
|
||||
'kibana.rac.alert.owner': 'apm',
|
||||
'kibana.rac.alert.status': 'open',
|
||||
});
|
||||
|
||||
describe('getAlertByIdRoute', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
||||
beforeEach(async () => {
|
||||
server = serverMock.create();
|
||||
({ clients, context } = requestContextMock.createTools());
|
||||
|
||||
clients.rac.get.mockResolvedValue(getMockAlert());
|
||||
|
||||
getAlertByIdRoute(server.router);
|
||||
});
|
||||
|
||||
test('returns 200 when finding a single alert with valid params', async () => {
|
||||
const response = await server.inject(getReadRequest(), context);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(getMockAlert());
|
||||
});
|
||||
|
||||
test('returns 200 when finding a single alert with index param', async () => {
|
||||
const response = await server.inject(
|
||||
requestMock.create({
|
||||
method: 'get',
|
||||
path: BASE_RAC_ALERTS_API_PATH,
|
||||
query: { id: 'alert-1', index: '.alerts-me' },
|
||||
}),
|
||||
context
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(getMockAlert());
|
||||
});
|
||||
|
||||
describe('request validation', () => {
|
||||
test('rejects invalid query params', async () => {
|
||||
await expect(
|
||||
server.inject(
|
||||
requestMock.create({
|
||||
method: 'get',
|
||||
path: BASE_RAC_ALERTS_API_PATH,
|
||||
query: { id: 4 },
|
||||
}),
|
||||
context
|
||||
)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Request was rejected with message: 'Invalid value \\"4\\" supplied to \\"id\\"'"`
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects unknown query params', async () => {
|
||||
await expect(
|
||||
server.inject(
|
||||
requestMock.create({
|
||||
method: 'get',
|
||||
path: BASE_RAC_ALERTS_API_PATH,
|
||||
query: { notId: 4 },
|
||||
}),
|
||||
context
|
||||
)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Request was rejected with message: 'Invalid value \\"undefined\\" supplied to \\"id\\"'"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns error status if rac client "GET" fails', async () => {
|
||||
clients.rac.get.mockRejectedValue(new Error('Unable to get alert'));
|
||||
const response = await server.inject(getReadRequest(), context);
|
||||
|
||||
expect(response.status).toEqual(500);
|
||||
expect(response.body).toEqual({
|
||||
attributes: { success: false },
|
||||
message: 'Unable to get alert',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { IRouter } from 'kibana/server';
|
||||
import * as t from 'io-ts';
|
||||
import { id as _id } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { RacRequestHandlerContext } from '../types';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants';
|
||||
import { buildRouteValidation } from './utils/route_validation';
|
||||
|
||||
export const getAlertByIdRoute = (router: IRouter<RacRequestHandlerContext>) => {
|
||||
router.get(
|
||||
{
|
||||
path: BASE_RAC_ALERTS_API_PATH,
|
||||
validate: {
|
||||
query: buildRouteValidation(
|
||||
t.intersection([
|
||||
t.exact(
|
||||
t.type({
|
||||
id: _id,
|
||||
})
|
||||
),
|
||||
t.exact(
|
||||
t.partial({
|
||||
index: t.string,
|
||||
})
|
||||
),
|
||||
])
|
||||
),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:rac'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
try {
|
||||
const alertsClient = await context.rac.getAlertsClient();
|
||||
const { id, index } = request.query;
|
||||
const alert = await alertsClient.get({ id, index });
|
||||
if (alert == null) {
|
||||
return response.notFound({
|
||||
body: { message: `alert with id ${id} and index ${index} not found` },
|
||||
});
|
||||
}
|
||||
return response.ok({
|
||||
body: alert,
|
||||
});
|
||||
} 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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { IRouter } from 'kibana/server';
|
||||
import { id as _id } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { RacRequestHandlerContext } from '../types';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants';
|
||||
import { validFeatureIds } from '../utils/rbac';
|
||||
|
||||
export const getAlertsIndexRoute = (router: IRouter<RacRequestHandlerContext>) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${BASE_RAC_ALERTS_API_PATH}/index`,
|
||||
validate: false,
|
||||
options: {
|
||||
tags: ['access:rac'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
try {
|
||||
const alertsClient = await context.rac.getAlertsClient();
|
||||
const indexName = await alertsClient.getAuthorizedAlertsIndices(validFeatureIds);
|
||||
return response.ok({
|
||||
body: { index_name: indexName },
|
||||
});
|
||||
} catch (exc) {
|
||||
const err = transformError(exc);
|
||||
const contentType = {
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
const defaultedHeaders = {
|
||||
...contentType,
|
||||
};
|
||||
|
||||
return response.custom({
|
||||
headers: defaultedHeaders,
|
||||
statusCode: err.statusCode,
|
||||
body: Buffer.from(
|
||||
JSON.stringify({
|
||||
message: err.message,
|
||||
status_code: err.statusCode,
|
||||
})
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
18
x-pack/plugins/rule_registry/server/routes/index.ts
Normal file
18
x-pack/plugins/rule_registry/server/routes/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { IRouter } from 'kibana/server';
|
||||
import { RacRequestHandlerContext } from '../types';
|
||||
import { getAlertByIdRoute } from './get_alert_by_id';
|
||||
import { updateAlertByIdRoute } from './update_alert_by_id';
|
||||
import { getAlertsIndexRoute } from './get_alert_index';
|
||||
|
||||
export function defineRoutes(router: IRouter<RacRequestHandlerContext>) {
|
||||
getAlertByIdRoute(router);
|
||||
updateAlertByIdRoute(router);
|
||||
getAlertsIndexRoute(router);
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { updateAlertByIdRoute } from './update_alert_by_id';
|
||||
import { requestContextMock } from './__mocks__/request_context';
|
||||
import { getUpdateRequest } from './__mocks__/request_responses';
|
||||
import { requestMock, serverMock } from './__mocks__/server';
|
||||
|
||||
describe('updateAlertByIdRoute', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
||||
beforeEach(async () => {
|
||||
server = serverMock.create();
|
||||
({ clients, context } = requestContextMock.createTools());
|
||||
|
||||
clients.rac.update.mockResolvedValue({
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_version: 'WzM2MiwyXQ==',
|
||||
result: 'updated',
|
||||
_shards: { total: 2, successful: 1, failed: 0 },
|
||||
_seq_no: 1,
|
||||
_primary_term: 1,
|
||||
});
|
||||
|
||||
updateAlertByIdRoute(server.router);
|
||||
});
|
||||
|
||||
test('returns 200 when updating a single alert with valid params', async () => {
|
||||
const response = await server.inject(getUpdateRequest(), context);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_version: 'WzM2MiwyXQ==',
|
||||
result: 'updated',
|
||||
_shards: { total: 2, successful: 1, failed: 0 },
|
||||
_seq_no: 1,
|
||||
_primary_term: 1,
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('request validation', () => {
|
||||
test('rejects invalid query params', async () => {
|
||||
await expect(
|
||||
server.inject(
|
||||
requestMock.create({
|
||||
method: 'patch',
|
||||
path: BASE_RAC_ALERTS_API_PATH,
|
||||
body: {
|
||||
status: 'closed',
|
||||
ids: 'alert-1',
|
||||
index: '.alerts-observability-apm*',
|
||||
},
|
||||
}),
|
||||
context
|
||||
)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Request was rejected with message: 'Invalid value \\"alert-1\\" supplied to \\"ids\\"'"`
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects unknown query params', async () => {
|
||||
await expect(
|
||||
server.inject(
|
||||
requestMock.create({
|
||||
method: 'patch',
|
||||
path: BASE_RAC_ALERTS_API_PATH,
|
||||
body: {
|
||||
notStatus: 'closed',
|
||||
ids: ['alert-1'],
|
||||
index: '.alerts-observability-apm*',
|
||||
},
|
||||
}),
|
||||
context
|
||||
)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Request was rejected with message: 'Invalid value \\"undefined\\" supplied to \\"status\\"'"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns error status if rac client "GET" fails', async () => {
|
||||
clients.rac.update.mockRejectedValue(new Error('Unable to update alert'));
|
||||
const response = await server.inject(getUpdateRequest(), context);
|
||||
|
||||
expect(response.status).toEqual(500);
|
||||
expect(response.body).toEqual({
|
||||
attributes: { success: false },
|
||||
message: 'Unable to update alert',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { IRouter } from 'kibana/server';
|
||||
import * as t from 'io-ts';
|
||||
import { id as _id } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { buildRouteValidation } from './utils/route_validation';
|
||||
import { RacRequestHandlerContext } from '../types';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants';
|
||||
|
||||
export const updateAlertByIdRoute = (router: IRouter<RacRequestHandlerContext>) => {
|
||||
router.post(
|
||||
{
|
||||
path: BASE_RAC_ALERTS_API_PATH,
|
||||
validate: {
|
||||
body: buildRouteValidation(
|
||||
t.intersection([
|
||||
t.exact(
|
||||
t.type({
|
||||
status: t.string,
|
||||
ids: t.array(t.string),
|
||||
index: t.string,
|
||||
})
|
||||
),
|
||||
t.exact(
|
||||
t.partial({
|
||||
_version: t.string,
|
||||
})
|
||||
),
|
||||
])
|
||||
),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:rac'],
|
||||
},
|
||||
},
|
||||
async (context, req, response) => {
|
||||
try {
|
||||
const alertsClient = await context.rac.getAlertsClient();
|
||||
const { status, ids, index, _version } = req.body;
|
||||
|
||||
const updatedAlert = await alertsClient.update({
|
||||
id: ids[0],
|
||||
status,
|
||||
_version,
|
||||
index,
|
||||
});
|
||||
|
||||
if (updatedAlert == null) {
|
||||
return response.notFound({
|
||||
body: { message: `alerts with ids ${ids} and index ${index} not found` },
|
||||
});
|
||||
}
|
||||
|
||||
return response.ok({ body: { success: true, ...updatedAlert } });
|
||||
} 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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* 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 { fold } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import * as rt from 'io-ts';
|
||||
import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils';
|
||||
|
||||
import {
|
||||
RouteValidationError,
|
||||
RouteValidationFunction,
|
||||
RouteValidationResultFactory,
|
||||
} from '../../../../../../src/core/server';
|
||||
|
||||
type RequestValidationResult<T> =
|
||||
| {
|
||||
value: T;
|
||||
error?: undefined;
|
||||
}
|
||||
| {
|
||||
value?: undefined;
|
||||
error: RouteValidationError;
|
||||
};
|
||||
|
||||
/**
|
||||
* Copied from x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts
|
||||
* This really should be in @kbn/securitysolution-io-ts-utils rather than copied yet again, however, this has types
|
||||
* from a lot of places such as RouteValidationResultFactory from core/server which in turn can pull in @kbn/schema
|
||||
* which cannot work on the front end and @kbn/securitysolution-io-ts-utils works on both front and backend.
|
||||
*
|
||||
* TODO: Figure out a way to move this function into a package rather than copying it/forking it within plugins
|
||||
*/
|
||||
export const buildRouteValidation = <T extends rt.Mixed, A = rt.TypeOf<T>>(
|
||||
schema: T
|
||||
): RouteValidationFunction<A> => (
|
||||
inputValue: unknown,
|
||||
validationResult: RouteValidationResultFactory
|
||||
): RequestValidationResult<A> =>
|
||||
pipe(
|
||||
schema.decode(inputValue),
|
||||
(decoded) => exactCheck(inputValue, decoded),
|
||||
fold<rt.Errors, A, RequestValidationResult<A>>(
|
||||
(errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()),
|
||||
(validatedInput: A) => validationResult.ok(validatedInput)
|
||||
)
|
||||
);
|
|
@ -11,6 +11,7 @@ import { ElasticsearchClient } from 'kibana/server';
|
|||
import { FieldDescriptor } from 'src/plugins/data/server';
|
||||
import { ESSearchRequest, ESSearchResponse } from 'src/core/types/elasticsearch';
|
||||
import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_field_names';
|
||||
import { ValidFeatureId } from '../utils/rbac';
|
||||
|
||||
export interface RuleDataReader {
|
||||
search<TSearchRequest extends ESSearchRequest>(
|
||||
|
@ -37,9 +38,17 @@ export interface IRuleDataClient {
|
|||
createWriteTargetIfNeeded(options: { namespace?: string }): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The purpose of the `feature` param is to force the user to update
|
||||
* the data structure which contains the mapping of consumers to alerts
|
||||
* as data indices. The idea is it is typed such that it forces the
|
||||
* user to go to the code and modify it. At least until a better system
|
||||
* is put in place or we move the alerts as data client out of rule registry.
|
||||
*/
|
||||
export interface RuleDataClientConstructorOptions {
|
||||
getClusterClient: () => Promise<ElasticsearchClient>;
|
||||
isWriteEnabled: boolean;
|
||||
ready: () => Promise<void>;
|
||||
alias: string;
|
||||
feature: ValidFeatureId;
|
||||
}
|
||||
|
|
|
@ -20,10 +20,11 @@ import { ClusterPutComponentTemplateBody, PutIndexTemplateRequest } from '../../
|
|||
import { RuleDataClient } from '../rule_data_client';
|
||||
import { RuleDataWriteDisabledError } from './errors';
|
||||
import { incrementIndexName } from './utils';
|
||||
import { ValidFeatureId } from '../utils/rbac';
|
||||
|
||||
const BOOTSTRAP_TIMEOUT = 60000;
|
||||
|
||||
interface RuleDataPluginServiceConstructorOptions {
|
||||
export interface RuleDataPluginServiceConstructorOptions {
|
||||
getClusterClient: () => Promise<ElasticsearchClient>;
|
||||
logger: Logger;
|
||||
isWriteEnabled: boolean;
|
||||
|
@ -223,9 +224,10 @@ export class RuleDataPluginService {
|
|||
return [this.options.index, assetName].filter(Boolean).join('-');
|
||||
}
|
||||
|
||||
getRuleDataClient(alias: string, initialize: () => Promise<void>) {
|
||||
getRuleDataClient(feature: ValidFeatureId, alias: string, initialize: () => Promise<void>) {
|
||||
return new RuleDataClient({
|
||||
alias,
|
||||
feature,
|
||||
getClusterClient: () => this.getClusterClient(),
|
||||
isWriteEnabled: this.isWriteEnabled(),
|
||||
ready: initialize,
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { RuleDataPluginService, RuleDataPluginServiceConstructorOptions } from './';
|
||||
|
||||
type Schema = PublicMethodsOf<RuleDataPluginService>;
|
||||
|
||||
const createRuleDataPluginServiceMock = (_: RuleDataPluginServiceConstructorOptions) => {
|
||||
const mocked: jest.Mocked<Schema> = {
|
||||
init: jest.fn(),
|
||||
isReady: jest.fn(),
|
||||
wait: jest.fn(),
|
||||
isWriteEnabled: jest.fn(),
|
||||
getFullAssetName: jest.fn(),
|
||||
createOrUpdateComponentTemplate: jest.fn(),
|
||||
createOrUpdateIndexTemplate: jest.fn(),
|
||||
createOrUpdateLifecyclePolicy: jest.fn(),
|
||||
getRuleDataClient: jest.fn(),
|
||||
updateIndexMappingsMatchingPattern: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const ruleDataPluginServiceMock: {
|
||||
create: (
|
||||
_: RuleDataPluginServiceConstructorOptions
|
||||
) => jest.Mocked<PublicMethodsOf<RuleDataPluginService>>;
|
||||
} = {
|
||||
create: createRuleDataPluginServiceMock,
|
||||
};
|
24
x-pack/plugins/rule_registry/server/scripts/README.md
Normal file
24
x-pack/plugins/rule_registry/server/scripts/README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
Users with roles granting them access to monitoring (observability) and siem (security solution) should only be able to access alerts with those roles
|
||||
|
||||
```bash
|
||||
myterminal~$ ./get_security_solution_alert.sh observer
|
||||
{
|
||||
"statusCode": 404,
|
||||
"error": "Not Found",
|
||||
"message": "Unauthorized to get \"rac:8.0.0:securitySolution/get\" alert\""
|
||||
}
|
||||
myterminal~$ ./get_security_solution_alert.sh
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
myterminal~$ ./get_observability_alert.sh
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
myterminal~$ ./get_observability_alert.sh hunter
|
||||
{
|
||||
"statusCode": 404,
|
||||
"error": "Not Found",
|
||||
"message": "Unauthorized to get \"rac:8.0.0:observability/get\" alert\""
|
||||
}
|
||||
```
|
23
x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh
Executable file
23
x-pack/plugins/rule_registry/server/scripts/get_alerts_index.sh
Executable file
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
USER=${1:-'observer'}
|
||||
|
||||
cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ..
|
||||
|
||||
# Example: ./find_rules.sh
|
||||
curl -v -k \
|
||||
-u $USER:changeme \
|
||||
-X GET "${KIBANA_URL}${SPACE_URL}/internal/rac/alerts/index" | jq .
|
||||
|
||||
# -X GET "${KIBANA_URL}${SPACE_URL}/api/apm/settings/apm-alerts-as-data-indices" | jq .
|
22
x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh
Executable file
22
x-pack/plugins/rule_registry/server/scripts/get_observability_alert.sh
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
USER=${1:-'observer'}
|
||||
ID=${2:-'DHEnOXoB8br9Z2X1fq_l'}
|
||||
|
||||
cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ..
|
||||
|
||||
# Example: ./get_observability_alert.sh hunter
|
||||
curl -v -k \
|
||||
-u $USER:changeme \
|
||||
-X GET "${KIBANA_URL}${SPACE_URL}/internal/rac/alerts?id=$ID&index=.alerts-observability-apm" | jq .
|
22
x-pack/plugins/rule_registry/server/scripts/get_security_alert.sh
Executable file
22
x-pack/plugins/rule_registry/server/scripts/get_security_alert.sh
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
USER=${1:-'hunter'}
|
||||
ID=${2:-'kdL4gHoBFALkyfScIsY5'}
|
||||
|
||||
cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ..
|
||||
|
||||
# Example: ./get_observability_alert.sh hunter
|
||||
curl -v -k \
|
||||
-u $USER:changeme \
|
||||
-X GET "${KIBANA_URL}${SPACE_URL}/internal/rac/alerts?id=$ID&index=.alerts-security-solution" | jq .
|
|
@ -0,0 +1,5 @@
|
|||
This user can access the monitoring route at http://localhost:5601/security-myfakepath
|
||||
|
||||
| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts |
|
||||
| :-----------------: | :----------: | :-------------------------------: | :---: | :--------------: | :---------------: | :------------: |
|
||||
| Hunter / T3 Analyst | read, write | read | read | read, write | read | read, write |
|
11
x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh
Executable file
11
x-pack/plugins/rule_registry/server/scripts/hunter/delete_detections_user.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-XDELETE ${ELASTICSEARCH_URL}/_security/user/hunter
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"elasticsearch": {
|
||||
"cluster": [],
|
||||
"indices": []
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"feature": {
|
||||
"ml": ["read"],
|
||||
"siem": ["all"],
|
||||
"actions": ["read"],
|
||||
"ruleRegistry": ["all"],
|
||||
"builtInAlerts": ["all"],
|
||||
"alerting": ["all"]
|
||||
},
|
||||
"spaces": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"password": "changeme",
|
||||
"roles": ["hunter"],
|
||||
"full_name": "Hunter",
|
||||
"email": "detections-reader@example.com"
|
||||
}
|
11
x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh
Executable file
11
x-pack/plugins/rule_registry/server/scripts/hunter/get_detections_role.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-XGET ${KIBANA_URL}/api/security/role/hunter | jq -S .
|
10
x-pack/plugins/rule_registry/server/scripts/hunter/index.ts
Normal file
10
x-pack/plugins/rule_registry/server/scripts/hunter/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 * as hunterUser from './detections_user.json';
|
||||
import * as hunterRole from './detections_role.json';
|
||||
export { hunterUser, hunterRole };
|
14
x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh
Executable file
14
x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_role.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
ROLE=(${@:-./detections_role.json})
|
||||
|
||||
curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-XPUT ${KIBANA_URL}/api/security/role/hunter \
|
||||
-d @${ROLE}
|
14
x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh
Executable file
14
x-pack/plugins/rule_registry/server/scripts/hunter/post_detections_user.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
USER=(${@:-./detections_user.json})
|
||||
|
||||
curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
${ELASTICSEARCH_URL}/_security/user/hunter \
|
||||
-d @${USER}
|
|
@ -0,0 +1,5 @@
|
|||
This user can access the monitoring route at http://localhost:5601/monitoring-myfakepath
|
||||
|
||||
| Role | Data Sources | Security Solution ML Jobs/Results | Lists | Rules/Exceptions | Action Connectors | Signals/Alerts |
|
||||
| :------: | :----------: | :-------------------------------: | :---: | :--------------: | :---------------: | :------------: |
|
||||
| observer | read, write | read | read | read, write | read | read, write |
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
curl -v -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-XDELETE ${ELASTICSEARCH_URL}/_security/user/observer
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"elasticsearch": {
|
||||
"cluster": [],
|
||||
"indices": []
|
||||
},
|
||||
"kibana": [
|
||||
{
|
||||
"feature": {
|
||||
"ml": ["read"],
|
||||
"monitoring": ["all"],
|
||||
"apm": ["minimal_read", "alerts_all"],
|
||||
"ruleRegistry": ["all"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["all"],
|
||||
"alerting": ["all"]
|
||||
},
|
||||
"spaces": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"password": "changeme",
|
||||
"roles": ["observer"],
|
||||
"full_name": "Observer",
|
||||
"email": "monitoring-observer@example.com"
|
||||
}
|
11
x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh
Executable file
11
x-pack/plugins/rule_registry/server/scripts/observer/get_detections_role.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-XGET ${KIBANA_URL}/api/security/role/hunter | jq -S .
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
USER=${1:-'observer'}
|
||||
|
||||
cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ..
|
||||
|
||||
# Example: ./find_rules.sh
|
||||
curl -s -k \
|
||||
-u $USER:changeme \
|
||||
-X GET ${KIBANA_URL}${SPACE_URL}/monitoring-myfakepath | jq .
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ..
|
||||
|
||||
|
||||
USER=${1:-'hunter'}
|
||||
|
||||
# Example: ./find_rules.sh
|
||||
curl -s -k \
|
||||
-u $USER:changeme \
|
||||
-X GET ${KIBANA_URL}${SPACE_URL}/security-myfakepath | jq .
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 * as observerUser from './detections_user.json';
|
||||
import * as observerRole from './detections_role.json';
|
||||
export { observerUser, observerRole };
|
14
x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh
Executable file
14
x-pack/plugins/rule_registry/server/scripts/observer/post_detections_role.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
ROLE=(${@:-./detections_role.json})
|
||||
|
||||
curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-XPUT ${KIBANA_URL}/api/security/role/observer \
|
||||
-d @${ROLE}
|
14
x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh
Executable file
14
x-pack/plugins/rule_registry/server/scripts/observer/post_detections_user.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
USER=(${@:-./detections_user.json})
|
||||
|
||||
curl -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'\
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
${ELASTICSEARCH_URL}/_security/user/observer \
|
||||
-d @${USER}
|
28
x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh
Executable file
28
x-pack/plugins/rule_registry/server/scripts/update_observability_alert.sh
Executable file
|
@ -0,0 +1,28 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
IDS=${1}
|
||||
STATUS=${2}
|
||||
|
||||
echo $IDS
|
||||
echo "'"$STATUS"'"
|
||||
|
||||
cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ..
|
||||
|
||||
# Example: ./update_observability_alert.sh [\"my-alert-id\",\"another-alert-id\"] <closed | open>
|
||||
curl -s -k \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u observer:changeme \
|
||||
-X POST ${KIBANA_URL}${SPACE_URL}/internal/rac/alerts \
|
||||
-d "{\"ids\": $IDS, \"status\":\"$STATUS\", \"index\":\".alerts-observability-apm\"}" | jq .
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RequestHandlerContext } from 'kibana/server';
|
||||
import {
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
|
@ -12,6 +13,7 @@ import {
|
|||
AlertTypeState,
|
||||
} from '../../alerting/common';
|
||||
import { AlertType } from '../../alerting/server';
|
||||
import { AlertsClient } from './alert_data_client/alerts_client';
|
||||
|
||||
type SimpleAlertType<
|
||||
TParams extends AlertTypeParams = {},
|
||||
|
@ -38,3 +40,17 @@ export type AlertTypeWithExecutor<
|
|||
> & {
|
||||
executor: AlertTypeExecutor<TParams, TAlertInstanceContext, TServices>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface RacApiRequestHandlerContext {
|
||||
getAlertsClient: () => Promise<AlertsClient>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface RacRequestHandlerContext extends RequestHandlerContext {
|
||||
rac: RacApiRequestHandlerContext;
|
||||
}
|
||||
|
|
|
@ -194,6 +194,7 @@ describe('createLifecycleRuleTypeFactory', () => {
|
|||
"event.kind": "event",
|
||||
"kibana.rac.alert.duration.us": 0,
|
||||
"kibana.rac.alert.id": "opbeans-java",
|
||||
"kibana.rac.alert.owner": "consumer",
|
||||
"kibana.rac.alert.producer": "test",
|
||||
"kibana.rac.alert.start": "2021-06-16T09:01:00.000Z",
|
||||
"kibana.rac.alert.status": "open",
|
||||
|
@ -212,6 +213,7 @@ describe('createLifecycleRuleTypeFactory', () => {
|
|||
"event.kind": "event",
|
||||
"kibana.rac.alert.duration.us": 0,
|
||||
"kibana.rac.alert.id": "opbeans-node",
|
||||
"kibana.rac.alert.owner": "consumer",
|
||||
"kibana.rac.alert.producer": "test",
|
||||
"kibana.rac.alert.start": "2021-06-16T09:01:00.000Z",
|
||||
"kibana.rac.alert.status": "open",
|
||||
|
@ -230,6 +232,7 @@ describe('createLifecycleRuleTypeFactory', () => {
|
|||
"event.kind": "signal",
|
||||
"kibana.rac.alert.duration.us": 0,
|
||||
"kibana.rac.alert.id": "opbeans-java",
|
||||
"kibana.rac.alert.owner": "consumer",
|
||||
"kibana.rac.alert.producer": "test",
|
||||
"kibana.rac.alert.start": "2021-06-16T09:01:00.000Z",
|
||||
"kibana.rac.alert.status": "open",
|
||||
|
@ -248,6 +251,7 @@ describe('createLifecycleRuleTypeFactory', () => {
|
|||
"event.kind": "signal",
|
||||
"kibana.rac.alert.duration.us": 0,
|
||||
"kibana.rac.alert.id": "opbeans-node",
|
||||
"kibana.rac.alert.owner": "consumer",
|
||||
"kibana.rac.alert.producer": "test",
|
||||
"kibana.rac.alert.start": "2021-06-16T09:01:00.000Z",
|
||||
"kibana.rac.alert.status": "open",
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
ALERT_UUID,
|
||||
EVENT_ACTION,
|
||||
EVENT_KIND,
|
||||
OWNER,
|
||||
RULE_UUID,
|
||||
TIMESTAMP,
|
||||
} from '../../common/technical_rule_data_field_names';
|
||||
|
@ -69,6 +70,7 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({
|
|||
const {
|
||||
services: { alertInstanceFactory },
|
||||
state: previousState,
|
||||
rule,
|
||||
} = options;
|
||||
|
||||
const ruleExecutorData = getRuleExecutorData(type, options);
|
||||
|
@ -180,6 +182,7 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({
|
|||
...ruleExecutorData,
|
||||
[TIMESTAMP]: timestamp,
|
||||
[EVENT_KIND]: 'event',
|
||||
[OWNER]: rule.consumer,
|
||||
[ALERT_ID]: alertId,
|
||||
};
|
||||
|
||||
|
@ -234,6 +237,7 @@ export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({
|
|||
[EVENT_KIND]: 'signal',
|
||||
});
|
||||
}
|
||||
logger.debug(`Preparing to index ${eventsToIndex.length} alerts.`);
|
||||
|
||||
if (ruleDataClient.isWriteEnabled()) {
|
||||
await ruleDataClient.getWriter().bulk({
|
||||
|
|
22
x-pack/plugins/rule_registry/server/utils/rbac.ts
Normal file
22
x-pack/plugins/rule_registry/server/utils/rbac.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* registering a new instance of the rule data client
|
||||
* in a new plugin will require updating the below data structure
|
||||
* to include the index name where the alerts as data will be written to.
|
||||
*/
|
||||
export const mapConsumerToIndexName = {
|
||||
apm: '.alerts-observability-apm',
|
||||
observability: '.alerts-observability',
|
||||
siem: ['.alerts-security.alerts', '.siem-signals'],
|
||||
};
|
||||
export type ValidFeatureId = keyof typeof mapConsumerToIndexName;
|
||||
|
||||
export const validFeatureIds = Object.keys(mapConsumerToIndexName);
|
||||
export const isValidFeatureId = (a: unknown): a is ValidFeatureId =>
|
||||
typeof a === 'string' && validFeatureIds.includes(a);
|
|
@ -7,11 +7,19 @@
|
|||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["common/**/*", "server/**/*", "public/**/*", "../../../typings/**/*"],
|
||||
"include": [
|
||||
"common/**/*",
|
||||
"server/**/*",
|
||||
// have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636
|
||||
"server/**/*.json",
|
||||
"public/**/*",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/data/tsconfig.json" },
|
||||
{ "path": "../alerting/tsconfig.json" },
|
||||
{ "path": "../security/tsconfig.json" },
|
||||
{ "path": "../spaces/tsconfig.json" },
|
||||
{ "path": "../triggers_actions_ui/tsconfig.json" }
|
||||
]
|
||||
|
|
|
@ -238,6 +238,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
});
|
||||
|
||||
ruleDataClient = ruleDataService.getRuleDataClient(
|
||||
SERVER_APP_ID,
|
||||
ruleDataService.getFullAssetName('security.alerts'),
|
||||
() => initializeRuleDataTemplatesPromise
|
||||
);
|
||||
|
@ -338,7 +339,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
all: {
|
||||
app: [APP_ID, 'kibana'],
|
||||
catalogue: ['securitySolution'],
|
||||
api: ['securitySolution', 'lists-all', 'lists-read'],
|
||||
api: ['securitySolution', 'lists-all', 'lists-read', 'rac'],
|
||||
savedObject: {
|
||||
all: ['alert', 'exception-list', 'exception-list-agnostic', ...savedObjectTypes],
|
||||
read: [],
|
||||
|
@ -359,7 +360,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
read: {
|
||||
app: [APP_ID, 'kibana'],
|
||||
catalogue: ['securitySolution'],
|
||||
api: ['securitySolution', 'lists-read'],
|
||||
api: ['securitySolution', 'lists-read', 'rac'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['exception-list', 'exception-list-agnostic', ...savedObjectTypes],
|
||||
|
|
|
@ -45,6 +45,9 @@ const onlyNotInCoverageTests = [
|
|||
require.resolve('../test/detection_engine_api_integration/basic/config.ts'),
|
||||
require.resolve('../test/lists_api_integration/security_and_spaces/config.ts'),
|
||||
require.resolve('../test/plugin_api_integration/config.ts'),
|
||||
require.resolve('../test/rule_registry/security_and_spaces/config_basic.ts'),
|
||||
require.resolve('../test/rule_registry/security_and_spaces/config_trial.ts'),
|
||||
require.resolve('../test/rule_registry/spaces_only/config_trial.ts'),
|
||||
require.resolve('../test/security_api_integration/saml.config.ts'),
|
||||
require.resolve('../test/security_api_integration/session_idle.config.ts'),
|
||||
require.resolve('../test/security_api_integration/session_invalidate.config.ts'),
|
||||
|
|
|
@ -21,7 +21,23 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
// If you're removing a privilege, this breaks backwards compatibility
|
||||
// Roles are associated with these privileges, and we shouldn't be removing them in a minor version.
|
||||
const expected = {
|
||||
global: ['all', 'read'],
|
||||
space: ['all', 'read'],
|
||||
features: {
|
||||
graph: ['all', 'read'],
|
||||
savedObjectsTagging: ['all', 'read'],
|
||||
canvas: ['all', 'read', 'minimal_all', 'minimal_read', 'generate_report'],
|
||||
maps: ['all', 'read'],
|
||||
fleet: ['all', 'read'],
|
||||
actions: ['all', 'read'],
|
||||
stackAlerts: ['all', 'read'],
|
||||
ml: ['all', 'read'],
|
||||
siem: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_all', 'cases_read'],
|
||||
observabilityCases: ['all', 'read'],
|
||||
uptime: ['all', 'read'],
|
||||
infrastructure: ['all', 'read'],
|
||||
logs: ['all', 'read'],
|
||||
apm: ['all', 'read', 'minimal_all', 'minimal_read', 'alerts_all', 'alerts_read'],
|
||||
discover: [
|
||||
'all',
|
||||
'read',
|
||||
|
@ -53,24 +69,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
advancedSettings: ['all', 'read'],
|
||||
indexPatterns: ['all', 'read'],
|
||||
savedObjectsManagement: ['all', 'read'],
|
||||
savedObjectsTagging: ['all', 'read'],
|
||||
timelion: ['all', 'read'],
|
||||
graph: ['all', 'read'],
|
||||
maps: ['all', 'read'],
|
||||
canvas: ['all', 'read', 'minimal_all', 'minimal_read', 'generate_report'],
|
||||
infrastructure: ['all', 'read'],
|
||||
logs: ['all', 'read'],
|
||||
observabilityCases: ['all', 'read'],
|
||||
uptime: ['all', 'read'],
|
||||
apm: ['all', 'read'],
|
||||
ml: ['all', 'read'],
|
||||
siem: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_all', 'cases_read'],
|
||||
fleet: ['all', 'read'],
|
||||
stackAlerts: ['all', 'read'],
|
||||
actions: ['all', 'read'],
|
||||
},
|
||||
global: ['all', 'read'],
|
||||
space: ['all', 'read'],
|
||||
reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
|
||||
};
|
||||
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { secOnly } from '../../../rule_registry/common/lib/authentication/users';
|
||||
import {
|
||||
createSpacesAndUsers,
|
||||
deleteSpacesAndUsers,
|
||||
} from '../../../rule_registry/common/lib/authentication/';
|
||||
import {
|
||||
Direction,
|
||||
TimelineEventsQueries,
|
||||
|
@ -407,10 +412,19 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const retry = getService('retry');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('Timeline', () => {
|
||||
before(() => esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'));
|
||||
after(() => esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'));
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts');
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts');
|
||||
await createSpacesAndUsers(getService);
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts');
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts');
|
||||
await deleteSpacesAndUsers(getService);
|
||||
});
|
||||
|
||||
it('Make sure that we get Timeline data', async () => {
|
||||
await retry.try(async () => {
|
||||
|
@ -454,6 +468,60 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
// TODO: unskip this test once authz is added to search strategy
|
||||
it.skip('Make sure that we get Timeline data using the hunter role and do not receive observability alerts', async () => {
|
||||
await retry.try(async () => {
|
||||
const requestBody = {
|
||||
defaultIndex: ['.alerts*'], // query both .alerts-observability-apm and .alerts-security-solution
|
||||
docValueFields: [],
|
||||
factoryQueryType: TimelineEventsQueries.all,
|
||||
fieldRequested: FIELD_REQUESTED,
|
||||
// fields: [],
|
||||
filterQuery: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
activePage: 0,
|
||||
querySize: 25,
|
||||
},
|
||||
language: 'kuery',
|
||||
sort: [
|
||||
{
|
||||
field: '@timestamp',
|
||||
direction: Direction.desc,
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
timerange: {
|
||||
from: FROM,
|
||||
to: TO,
|
||||
interval: '12h',
|
||||
},
|
||||
};
|
||||
const resp = await supertestWithoutAuth
|
||||
.post('/internal/search/securitySolutionTimelineSearchStrategy/')
|
||||
.auth(secOnly.username, secOnly.password) // using security 'hunter' role
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(requestBody)
|
||||
.expect(200);
|
||||
|
||||
const timeline = resp.body;
|
||||
|
||||
// we inject one alert into the security solutions alerts index and another alert into the observability alerts index
|
||||
// therefore when accessing the .alerts* index with the security solution user,
|
||||
// only security solution alerts should be returned since the security solution user
|
||||
// is not authorized to view observability alerts.
|
||||
expect(timeline.totalCount).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('Make sure that pagination is working in Timeline query', async () => {
|
||||
await retry.try(async () => {
|
||||
const resp = await supertest
|
||||
|
|
|
@ -367,6 +367,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
"kibana.rac.alert.id": Array [
|
||||
"apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED",
|
||||
],
|
||||
"kibana.rac.alert.owner": Array [
|
||||
"apm",
|
||||
],
|
||||
"kibana.rac.alert.producer": Array [
|
||||
"apm",
|
||||
],
|
||||
|
@ -437,6 +440,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
"kibana.rac.alert.id": Array [
|
||||
"apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED",
|
||||
],
|
||||
"kibana.rac.alert.owner": Array [
|
||||
"apm",
|
||||
],
|
||||
"kibana.rac.alert.producer": Array [
|
||||
"apm",
|
||||
],
|
||||
|
@ -541,6 +547,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
"kibana.rac.alert.id": Array [
|
||||
"apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED",
|
||||
],
|
||||
"kibana.rac.alert.owner": Array [
|
||||
"apm",
|
||||
],
|
||||
"kibana.rac.alert.producer": Array [
|
||||
"apm",
|
||||
],
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".alerts-observability-apm",
|
||||
"id": "NoxgpHkBqbdrfX07MqXV",
|
||||
"source": {
|
||||
"@timestamp": "2020-12-16T15:16:18.570Z",
|
||||
"rule.id": "apm.error_rate",
|
||||
"message": "hello world 1",
|
||||
"kibana.rac.alert.owner": "apm",
|
||||
"kibana.rac.alert.status": "open"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".alerts-security.alerts",
|
||||
"id": "020202",
|
||||
"source": {
|
||||
"@timestamp": "2020-12-16T15:16:18.570Z",
|
||||
"rule.id": "siem.signals",
|
||||
"message": "hello world security",
|
||||
"kibana.rac.alert.owner": "siem",
|
||||
"kibana.rac.alert.status": "open"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": ".alerts-observability-apm",
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
},
|
||||
"kibana.rac.alert.owner": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": ".alerts-security.alerts",
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
},
|
||||
"kibana.rac.alert.owner": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
94
x-pack/test/rule_registry/common/config.ts
Normal file
94
x-pack/test/rule_registry/common/config.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { CA_CERT_PATH } from '@kbn/dev-utils';
|
||||
import { FtrConfigProviderContext } from '@kbn/test';
|
||||
|
||||
import { services } from './services';
|
||||
import { getAllExternalServiceSimulatorPaths } from '../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin';
|
||||
|
||||
interface CreateTestConfigOptions {
|
||||
license: string;
|
||||
disabledPlugins?: string[];
|
||||
ssl?: boolean;
|
||||
testFiles?: string[];
|
||||
}
|
||||
|
||||
// test.not-enabled is specifically not enabled
|
||||
const enabledActionTypes = [
|
||||
'.email',
|
||||
'.index',
|
||||
'.jira',
|
||||
'.pagerduty',
|
||||
'.resilient',
|
||||
'.server-log',
|
||||
'.servicenow',
|
||||
'.servicenow-sir',
|
||||
'.slack',
|
||||
'.webhook',
|
||||
'.case',
|
||||
'test.authorization',
|
||||
'test.failing',
|
||||
'test.index-record',
|
||||
'test.noop',
|
||||
'test.rate-limit',
|
||||
];
|
||||
|
||||
export function createTestConfig(name: string, options: CreateTestConfigOptions) {
|
||||
const { license = 'trial', disabledPlugins = [], ssl = false, testFiles = [] } = options;
|
||||
|
||||
return async ({ readConfigFile }: FtrConfigProviderContext) => {
|
||||
const xPackApiIntegrationTestsConfig = await readConfigFile(
|
||||
require.resolve('../../api_integration/config.ts')
|
||||
);
|
||||
|
||||
const servers = {
|
||||
...xPackApiIntegrationTestsConfig.get('servers'),
|
||||
elasticsearch: {
|
||||
...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'),
|
||||
protocol: ssl ? 'https' : 'http',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
testFiles: testFiles ? testFiles : [require.resolve('../tests/common')],
|
||||
servers,
|
||||
services,
|
||||
junit: {
|
||||
reportName: 'X-Pack Rule Registry Alerts Client API Integration Tests',
|
||||
},
|
||||
esTestCluster: {
|
||||
...xPackApiIntegrationTestsConfig.get('esTestCluster'),
|
||||
license,
|
||||
ssl,
|
||||
serverArgs: [
|
||||
`xpack.license.self_generated.type=${license}`,
|
||||
`xpack.security.enabled=${
|
||||
!disabledPlugins.includes('security') && ['trial', 'basic'].includes(license)
|
||||
}`,
|
||||
],
|
||||
},
|
||||
kbnTestServer: {
|
||||
...xPackApiIntegrationTestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
`--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`,
|
||||
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
|
||||
'--xpack.eventLog.logEntries=true',
|
||||
...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`),
|
||||
`--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`,
|
||||
...(ssl
|
||||
? [
|
||||
`--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`,
|
||||
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
12
x-pack/test/rule_registry/common/ftr_provider_context.d.ts
vendored
Normal file
12
x-pack/test/rule_registry/common/ftr_provider_context.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { GenericFtrProviderContext } from '@kbn/test';
|
||||
|
||||
import { services } from './services';
|
||||
|
||||
export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>;
|
105
x-pack/test/rule_registry/common/lib/authentication/index.ts
Normal file
105
x-pack/test/rule_registry/common/lib/authentication/index.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { Role, User, UserInfo } from './types';
|
||||
import { allUsers } from './users';
|
||||
import { allRoles } from './roles';
|
||||
import { spaces } from './spaces';
|
||||
|
||||
export const getUserInfo = (user: User): UserInfo => ({
|
||||
username: user.username,
|
||||
full_name: user.username.replace('_', ' '),
|
||||
email: `${user.username}@elastic.co`,
|
||||
});
|
||||
|
||||
export const createSpaces = async (getService: CommonFtrProviderContext['getService']) => {
|
||||
const spacesService = getService('spaces');
|
||||
for (const space of spaces) {
|
||||
await spacesService.create(space);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the users and roles for use in the tests. Defaults to specific users and roles used by the security_and_spaces
|
||||
* scenarios but can be passed specific ones as well.
|
||||
*/
|
||||
export const createUsersAndRoles = async (
|
||||
getService: CommonFtrProviderContext['getService'],
|
||||
usersToCreate: User[] = allUsers,
|
||||
rolesToCreate: Role[] = allRoles
|
||||
) => {
|
||||
const security = getService('security');
|
||||
|
||||
const createRole = async ({ name, privileges }: Role) => {
|
||||
return security.role.create(name, privileges);
|
||||
};
|
||||
|
||||
const createUser = async (user: User) => {
|
||||
const userInfo = getUserInfo(user);
|
||||
|
||||
return security.user.create(user.username, {
|
||||
password: user.password,
|
||||
roles: user.roles,
|
||||
full_name: userInfo.full_name,
|
||||
email: userInfo.email,
|
||||
});
|
||||
};
|
||||
|
||||
for (const role of rolesToCreate) {
|
||||
await createRole(role);
|
||||
}
|
||||
|
||||
for (const user of usersToCreate) {
|
||||
await createUser(user);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteSpaces = async (getService: CommonFtrProviderContext['getService']) => {
|
||||
const spacesService = getService('spaces');
|
||||
for (const space of spaces) {
|
||||
try {
|
||||
await spacesService.delete(space.id);
|
||||
} catch (error) {
|
||||
// ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUsersAndRoles = async (
|
||||
getService: CommonFtrProviderContext['getService'],
|
||||
usersToDelete: User[] = allUsers,
|
||||
rolesToDelete: Role[] = allRoles
|
||||
) => {
|
||||
const security = getService('security');
|
||||
|
||||
for (const user of usersToDelete) {
|
||||
try {
|
||||
await security.user.delete(user.username);
|
||||
} catch (error) {
|
||||
// ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users
|
||||
}
|
||||
}
|
||||
|
||||
for (const role of rolesToDelete) {
|
||||
try {
|
||||
await security.role.delete(role.name);
|
||||
} catch (error) {
|
||||
// ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createSpacesAndUsers = async (getService: CommonFtrProviderContext['getService']) => {
|
||||
await createSpaces(getService);
|
||||
await createUsersAndRoles(getService);
|
||||
};
|
||||
|
||||
export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext['getService']) => {
|
||||
await deleteSpaces(getService);
|
||||
await deleteUsersAndRoles(getService);
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue