Shim alerting plugin for new platform (#46954)

* Routes to register inside the init function

* Start shimming plugins and core

* More shimming

* Move code over to plugin.ts

* Use new platform basePath service

* Cleanup

* Create alerts client factory

* Move some functions to plugin start

* Remove some dependencies from Hapi

* Apply PR feedback; use new platform logger, rename interfaces

* Use new platform mocks for logger

* Use new platform elasticsearch service

* Fix type check failures

* Fix failing jest tests

* Remove logger from provided alert type service

* Add unit tests for alerts client factory

* Cleanup and fix type check errors

* Change structure exposed via plugin

* Use shim file, split plugin dependencies between setup and start contract, new task runner factory

* Apply PR feedback

* Fix ESLint errors

* Set return type on plugin functions

* Typo

* Fix typecheck errors

* IClusterClient
This commit is contained in:
Mike Côté 2019-10-07 18:15:41 -04:00 committed by GitHub
parent a691ba267f
commit cbc2b1ad52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1080 additions and 769 deletions

View file

@ -37,7 +37,7 @@ When security is enabled, users who create alerts will need the `manage_api_key`
### Methods
**server.plugins.alerting.registerType(options)**
**server.plugins.alerting.setup.registerType(options)**
The following table describes the properties of the `options` object.
@ -71,7 +71,7 @@ This example receives server and threshold as parameters. It will read the CPU u
```
import { schema } from '@kbn/config-schema';
...
server.plugins.alerting.registerType({
server.plugins.alerting.setup.registerType({
id: 'my-alert-type',
name: 'My alert type',
validate: {
@ -129,7 +129,7 @@ server.plugins.alerting.registerType({
This example only receives threshold as a parameter. It will read the CPU usage of all the servers and schedule individual actions if the reading for a server is greater than the threshold. This is a better implementation than above as only one query is performed for all the servers instead of one query per server.
```
server.plugins.alerting.registerType({
server.plugins.alerting.setup.registerType({
id: 'my-alert-type',
name: 'My alert type',
validate: {

View file

@ -9,7 +9,14 @@ import { Root } from 'joi';
import { init } from './server';
import mappings from './mappings.json';
export { AlertingPlugin, AlertsClient, AlertType, AlertExecutorOptions } from './server';
export {
AlertingPlugin,
AlertsClient,
AlertType,
AlertExecutorOptions,
PluginSetupContract,
PluginStartContract,
} from './server';
export function alerting(kibana: any) {
return new kibana.Plugin({

View file

@ -4,31 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('./lib/get_create_task_runner_function', () => ({
getCreateTaskRunnerFunction: jest.fn().mockReturnValue(jest.fn()),
}));
import { TaskRunnerFactory } from './lib';
import { AlertTypeRegistry } from './alert_type_registry';
import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks';
import { taskManagerMock } from '../../task_manager/task_manager.mock';
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/plugin.mock';
const taskManager = taskManagerMock.create();
const alertTypeRegistryParams = {
isSecurityEnabled: true,
getServices() {
return {
log: jest.fn(),
callCluster: jest.fn(),
savedObjectsClient: SavedObjectsClientMock.create(),
};
},
taskManager,
executeAction: jest.fn(),
getBasePath: jest.fn().mockReturnValue(undefined),
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.create(),
taskRunnerFactory: new TaskRunnerFactory(),
};
beforeEach(() => jest.resetAllMocks());
@ -60,31 +44,20 @@ describe('register()', () => {
executor: jest.fn(),
};
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getCreateTaskRunnerFunction } = require('./lib/get_create_task_runner_function');
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
getCreateTaskRunnerFunction.mockReturnValue(jest.fn());
registry.register(alertType);
expect(taskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1);
expect(taskManager.registerTaskDefinitions.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"alerting:test": Object {
"createTaskRunner": [MockFunction],
"createTaskRunner": [Function],
"title": "Test",
"type": "alerting:test",
},
},
]
`);
expect(getCreateTaskRunnerFunction).toHaveBeenCalledWith({
alertType,
isSecurityEnabled: true,
getServices: alertTypeRegistryParams.getServices,
encryptedSavedObjectsPlugin: alertTypeRegistryParams.encryptedSavedObjectsPlugin,
getBasePath: alertTypeRegistryParams.getBasePath,
spaceIdToNamespace: alertTypeRegistryParams.spaceIdToNamespace,
executeAction: alertTypeRegistryParams.executeAction,
});
});
test('should throw an error if type is already registered', () => {

View file

@ -6,53 +6,24 @@
import Boom from 'boom';
import { i18n } from '@kbn/i18n';
import { TaskManager } from '../../task_manager';
import { getCreateTaskRunnerFunction } from './lib';
import { ActionsPlugin } from '../../actions';
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
import {
AlertType,
GetBasePathFunction,
GetServicesFunction,
SpaceIdToNamespaceFunction,
} from './types';
import { TaskRunnerFactory } from './lib';
import { RunContext } from '../../task_manager';
import { TaskManagerSetupContract } from './shim';
import { AlertType } from './types';
interface ConstructorOptions {
isSecurityEnabled: boolean;
getServices: GetServicesFunction;
taskManager: TaskManager;
executeAction: ActionsPlugin['execute'];
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
spaceIdToNamespace: SpaceIdToNamespaceFunction;
getBasePath: GetBasePathFunction;
taskManager: TaskManagerSetupContract;
taskRunnerFactory: TaskRunnerFactory;
}
export class AlertTypeRegistry {
private readonly getServices: GetServicesFunction;
private readonly taskManager: TaskManager;
private readonly executeAction: ActionsPlugin['execute'];
private readonly taskManager: TaskManagerSetupContract;
private readonly alertTypes: Map<string, AlertType> = new Map();
private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
private readonly spaceIdToNamespace: SpaceIdToNamespaceFunction;
private readonly getBasePath: GetBasePathFunction;
private readonly isSecurityEnabled: boolean;
private readonly taskRunnerFactory: TaskRunnerFactory;
constructor({
encryptedSavedObjectsPlugin,
executeAction,
taskManager,
getServices,
spaceIdToNamespace,
getBasePath,
isSecurityEnabled,
}: ConstructorOptions) {
constructor({ taskManager, taskRunnerFactory }: ConstructorOptions) {
this.taskManager = taskManager;
this.executeAction = executeAction;
this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin;
this.getServices = getServices;
this.getBasePath = getBasePath;
this.spaceIdToNamespace = spaceIdToNamespace;
this.isSecurityEnabled = isSecurityEnabled;
this.taskRunnerFactory = taskRunnerFactory;
}
public has(id: string) {
@ -75,15 +46,8 @@ export class AlertTypeRegistry {
[`alerting:${alertType.id}`]: {
title: alertType.name,
type: `alerting:${alertType.id}`,
createTaskRunner: getCreateTaskRunnerFunction({
alertType,
isSecurityEnabled: this.isSecurityEnabled,
getServices: this.getServices,
executeAction: this.executeAction,
encryptedSavedObjectsPlugin: this.encryptedSavedObjectsPlugin,
getBasePath: this.getBasePath,
spaceIdToNamespace: this.spaceIdToNamespace,
}),
createTaskRunner: (context: RunContext) =>
this.taskRunnerFactory.create(alertType, context),
},
});
}

View file

@ -6,7 +6,7 @@
import { schema } from '@kbn/config-schema';
import { AlertsClient } from './alerts_client';
import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks';
import { SavedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks';
import { taskManagerMock } from '../../task_manager/task_manager.mock';
import { alertTypeRegistryMock } from './alert_type_registry.mock';
@ -15,13 +15,13 @@ const alertTypeRegistry = alertTypeRegistryMock.create();
const savedObjectsClient = SavedObjectsClientMock.create();
const alertsClientParams = {
log: jest.fn(),
taskManager,
alertTypeRegistry,
savedObjectsClient,
spaceId: 'default',
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingServiceMock.create().get(),
};
beforeEach(() => {
@ -418,16 +418,9 @@ describe('create()', () => {
await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Task manager error"`
);
expect(alertsClientParams.log).toHaveBeenCalledTimes(1);
expect(alertsClientParams.log.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Array [
"alerting",
"error",
],
"Failed to cleanup alert \\"1\\" after scheduling task failed. Error: Saved object delete error",
]
`);
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
'Failed to cleanup alert "1" after scheduling task failed. Error: Saved object delete error'
);
});
test('throws an error if alert type not registerd', async () => {

View file

@ -7,9 +7,9 @@
import Boom from 'boom';
import { omit } from 'lodash';
import { i18n } from '@kbn/i18n';
import { SavedObjectsClientContract, SavedObjectReference } from 'src/core/server';
import { Alert, RawAlert, AlertTypeRegistry, AlertAction, Log, AlertType } from './types';
import { TaskManager } from '../../task_manager';
import { Logger, SavedObjectsClientContract, SavedObjectReference } from 'src/core/server';
import { Alert, RawAlert, AlertTypeRegistry, AlertAction, AlertType } from './types';
import { TaskManagerStartContract } from './shim';
import { validateAlertTypeParams } from './lib';
import { CreateAPIKeyResult as SecurityPluginCreateAPIKeyResult } from '../../../../plugins/security/server';
@ -23,8 +23,8 @@ interface SuccessCreateAPIKeyResult {
export type CreateAPIKeyResult = FailedCreateAPIKeyResult | SuccessCreateAPIKeyResult;
interface ConstructorOptions {
log: Log;
taskManager: TaskManager;
logger: Logger;
taskManager: TaskManagerStartContract;
savedObjectsClient: SavedObjectsClientContract;
alertTypeRegistry: AlertTypeRegistry;
spaceId?: string;
@ -78,10 +78,10 @@ interface UpdateOptions {
}
export class AlertsClient {
private readonly log: Log;
private readonly logger: Logger;
private readonly getUserName: () => Promise<string | null>;
private readonly spaceId?: string;
private readonly taskManager: TaskManager;
private readonly taskManager: TaskManagerStartContract;
private readonly savedObjectsClient: SavedObjectsClientContract;
private readonly alertTypeRegistry: AlertTypeRegistry;
private readonly createAPIKey: () => Promise<CreateAPIKeyResult>;
@ -90,12 +90,12 @@ export class AlertsClient {
alertTypeRegistry,
savedObjectsClient,
taskManager,
log,
logger,
spaceId,
getUserName,
createAPIKey,
}: ConstructorOptions) {
this.log = log;
this.logger = logger;
this.getUserName = getUserName;
this.spaceId = spaceId;
this.taskManager = taskManager;
@ -143,8 +143,7 @@ export class AlertsClient {
await this.savedObjectsClient.delete('alert', createdAlert.id);
} catch (err) {
// Skip the cleanup error and throw the task manager error to avoid confusion
this.log(
['alerting', 'error'],
this.logger.error(
`Failed to cleanup alert "${createdAlert.id}" after scheduling task failed. Error: ${err.message}`
);
}

View file

@ -7,3 +7,4 @@
export { init } from './init';
export { AlertType, AlertingPlugin, AlertExecutorOptions } from './types';
export { AlertsClient } from './alerts_client';
export { PluginSetupContract, PluginStartContract } from './plugin';

View file

@ -4,181 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Hapi from 'hapi';
import uuid from 'uuid';
import { Legacy } from 'kibana';
import KbnServer from 'src/legacy/server/kbn_server';
import { ActionsPlugin } from '../../actions';
import { TaskManager } from '../../task_manager';
import { AlertingPlugin, Services } from './types';
import { AlertTypeRegistry } from './alert_type_registry';
import { AlertsClient, CreateAPIKeyResult } from './alerts_client';
import { SpacesPlugin } from '../../spaces';
import { KibanaRequest } from '../../../../../src/core/server';
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
import { PluginSetupContract as SecurityPluginSetupContract } from '../../../../plugins/security/server';
import { createOptionalPlugin } from '../../../server/lib/optional_plugin';
import {
createAlertRoute,
deleteAlertRoute,
findRoute,
getRoute,
listAlertTypesRoute,
updateAlertRoute,
enableAlertRoute,
disableAlertRoute,
updateApiKeyRoute,
muteAllAlertRoute,
unmuteAllAlertRoute,
muteAlertInstanceRoute,
unmuteAlertInstanceRoute,
} from './routes';
import { Server, shim } from './shim';
import { Plugin } from './plugin';
import { AlertingPlugin } from './types';
// Extend PluginProperties to indicate which plugins are guaranteed to exist
// due to being marked as dependencies
interface Plugins extends Hapi.PluginProperties {
actions: ActionsPlugin;
task_manager: TaskManager;
encrypted_saved_objects: EncryptedSavedObjectsPlugin;
}
export async function init(server: Server) {
const { initializerContext, coreSetup, coreStart, pluginsSetup, pluginsStart } = shim(server);
interface Server extends Legacy.Server {
plugins: Plugins;
}
const plugin = new Plugin(initializerContext);
export function init(server: Server) {
const config = server.config();
const kbnServer = (server as unknown) as KbnServer;
const taskManager = server.plugins.task_manager;
const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin');
const spaces = createOptionalPlugin<SpacesPlugin>(
config,
'xpack.spaces',
server.plugins,
'spaces'
);
const security = createOptionalPlugin<SecurityPluginSetupContract>(
config,
'xpack.security',
kbnServer.newPlatform.setup.plugins,
'security'
);
const setupContract = await plugin.setup(coreSetup, pluginsSetup);
const startContract = plugin.start(coreStart, pluginsStart);
server.plugins.xpack_main.registerFeature({
id: 'alerting',
name: 'Alerting',
app: ['alerting', 'kibana'],
privileges: {
all: {
savedObject: {
all: ['alert'],
read: [],
},
ui: [],
api: ['alerting-read', 'alerting-all'],
},
read: {
savedObject: {
all: [],
read: ['alert'],
},
ui: [],
api: ['alerting-read'],
},
},
});
// Encrypted attributes
server.plugins.encrypted_saved_objects.registerType({
type: 'alert',
attributesToEncrypt: new Set(['apiKey']),
attributesToExcludeFromAAD: new Set([
'scheduledTaskId',
'muted',
'mutedInstanceIds',
'updatedBy',
]),
});
function getServices(request: any): Services {
return {
log: (...args) => server.log(...args),
callCluster: (...args) => callWithRequest(request, ...args),
savedObjectsClient: server.savedObjects.getScopedSavedObjectsClient(request),
};
}
function getBasePath(spaceId?: string): string {
return spaces.isEnabled && spaceId
? spaces.getBasePath(spaceId)
: ((server.config().get('server.basePath') || '') as string);
}
function spaceIdToNamespace(spaceId?: string): string | undefined {
return spaces.isEnabled && spaceId ? spaces.spaceIdToNamespace(spaceId) : undefined;
}
const alertTypeRegistry = new AlertTypeRegistry({
getServices,
isSecurityEnabled: security.isEnabled,
taskManager,
executeAction: server.plugins.actions.execute,
encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects,
getBasePath,
spaceIdToNamespace,
});
// Register routes
createAlertRoute(server);
deleteAlertRoute(server);
findRoute(server);
getRoute(server);
listAlertTypesRoute(server);
updateAlertRoute(server);
enableAlertRoute(server);
disableAlertRoute(server);
updateApiKeyRoute(server);
muteAllAlertRoute(server);
unmuteAllAlertRoute(server);
muteAlertInstanceRoute(server);
unmuteAlertInstanceRoute(server);
// Expose functions
server.decorate('request', 'getAlertsClient', function() {
const request = this;
const savedObjectsClient = request.getSavedObjectsClient();
const alertsClient = new AlertsClient({
log: server.log.bind(server),
savedObjectsClient,
alertTypeRegistry,
taskManager,
spaceId: spaces.isEnabled ? spaces.getSpaceId(request) : undefined,
async getUserName(): Promise<string | null> {
if (!security.isEnabled) {
return null;
}
const user = await security.authc.getCurrentUser(KibanaRequest.from(request));
return user ? user.username : null;
},
async createAPIKey(): Promise<CreateAPIKeyResult> {
if (!security.isEnabled) {
return { created: false };
}
return {
created: true,
result: (await security.authc.createAPIKey(KibanaRequest.from(request), {
name: `source: alerting, generated uuid: "${uuid.v4()}"`,
role_descriptors: {},
}))!,
};
},
});
return alertsClient;
return startContract.getAlertsClientWithRequest(this);
});
const exposedFunctions: AlertingPlugin = {
registerType: alertTypeRegistry.register.bind(alertTypeRegistry),
listTypes: alertTypeRegistry.list.bind(alertTypeRegistry),
setup: setupContract,
start: startContract,
};
server.expose(exposedFunctions);
}

View file

@ -0,0 +1,107 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Request } from 'hapi';
import { AlertsClientFactory, ConstructorOpts } from './alerts_client_factory';
import { alertTypeRegistryMock } from '../alert_type_registry.mock';
import { taskManagerMock } from '../../../task_manager/task_manager.mock';
import { KibanaRequest } from '../../../../../../src/core/server';
import { loggingServiceMock } from '../../../../../../src/core/server/mocks';
jest.mock('../alerts_client');
const savedObjectsClient = jest.fn();
const securityPluginSetup = {
authc: {
createAPIKey: jest.fn(),
getCurrentUser: jest.fn(),
},
};
const alertsClientFactoryParams: jest.Mocked<ConstructorOpts> = {
logger: loggingServiceMock.create().get(),
taskManager: taskManagerMock.create(),
alertTypeRegistry: alertTypeRegistryMock.create(),
getSpaceId: jest.fn(),
};
const fakeRequest: Request = {
headers: {},
getBasePath: () => '',
path: '/',
route: { settings: {} },
url: {
href: '/',
},
raw: {
req: {
url: '/',
},
},
getSavedObjectsClient: () => savedObjectsClient,
} as any;
beforeEach(() => {
jest.resetAllMocks();
alertsClientFactoryParams.getSpaceId.mockReturnValue('default');
});
test('creates an alerts client with proper constructor arguments', async () => {
const factory = new AlertsClientFactory(alertsClientFactoryParams);
factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
expect(jest.requireMock('../alerts_client').AlertsClient).toHaveBeenCalledWith({
savedObjectsClient,
logger: alertsClientFactoryParams.logger,
taskManager: alertsClientFactoryParams.taskManager,
alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry,
spaceId: 'default',
getUserName: expect.any(Function),
createAPIKey: expect.any(Function),
});
});
test('getUserName() returns null when security is disabled', async () => {
const factory = new AlertsClientFactory(alertsClientFactoryParams);
factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
const userNameResult = await constructorCall.getUserName();
expect(userNameResult).toEqual(null);
});
test('getUserName() returns a name when security is enabled', async () => {
const factory = new AlertsClientFactory({
...alertsClientFactoryParams,
securityPluginSetup: securityPluginSetup as any,
});
factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
securityPluginSetup.authc.getCurrentUser.mockResolvedValueOnce({ username: 'bob' });
const userNameResult = await constructorCall.getUserName();
expect(userNameResult).toEqual('bob');
});
test('createAPIKey() returns { created: false } when security is disabled', async () => {
const factory = new AlertsClientFactory(alertsClientFactoryParams);
factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
const createAPIKeyResult = await constructorCall.createAPIKey();
expect(createAPIKeyResult).toEqual({ created: false });
});
test('createAPIKey() returns an API key when security is enabled', async () => {
const factory = new AlertsClientFactory({
...alertsClientFactoryParams,
securityPluginSetup: securityPluginSetup as any,
});
factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
securityPluginSetup.authc.createAPIKey.mockResolvedValueOnce({ api_key: '123', id: 'abc' });
const createAPIKeyResult = await constructorCall.createAPIKey();
expect(createAPIKeyResult).toEqual({ created: true, result: { api_key: '123', id: 'abc' } });
});

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Hapi from 'hapi';
import uuid from 'uuid';
import { AlertTypeRegistry } from '../types';
import { AlertsClient } from '../alerts_client';
import { SecurityPluginStartContract, TaskManagerStartContract } from '../shim';
import { KibanaRequest, Logger } from '../../../../../../src/core/server';
export interface ConstructorOpts {
logger: Logger;
taskManager: TaskManagerStartContract;
alertTypeRegistry: AlertTypeRegistry;
securityPluginSetup?: SecurityPluginStartContract;
getSpaceId: (request: Hapi.Request) => string | undefined;
}
export class AlertsClientFactory {
private readonly logger: Logger;
private readonly taskManager: TaskManagerStartContract;
private readonly alertTypeRegistry: AlertTypeRegistry;
private readonly securityPluginSetup?: SecurityPluginStartContract;
private readonly getSpaceId: (request: Hapi.Request) => string | undefined;
constructor(options: ConstructorOpts) {
this.logger = options.logger;
this.getSpaceId = options.getSpaceId;
this.taskManager = options.taskManager;
this.alertTypeRegistry = options.alertTypeRegistry;
this.securityPluginSetup = options.securityPluginSetup;
}
public create(request: KibanaRequest, legacyRequest: Hapi.Request): AlertsClient {
const { securityPluginSetup } = this;
return new AlertsClient({
logger: this.logger,
taskManager: this.taskManager,
alertTypeRegistry: this.alertTypeRegistry,
savedObjectsClient: legacyRequest.getSavedObjectsClient(),
spaceId: this.getSpaceId(legacyRequest),
async getUserName() {
if (!securityPluginSetup) {
return null;
}
const user = await securityPluginSetup.authc.getCurrentUser(request);
return user ? user.username : null;
},
async createAPIKey() {
if (!securityPluginSetup) {
return { created: false };
}
return {
created: true,
result: (await securityPluginSetup.authc.createAPIKey(request, {
name: `source: alerting, generated uuid: "${uuid.v4()}"`,
role_descriptors: {},
}))!,
};
},
});
}
}

View file

@ -6,6 +6,7 @@
import { AlertType } from '../types';
import { createExecutionHandler } from './create_execution_handler';
import { loggingServiceMock } from '../../../../../../src/core/server/mocks';
const alertType: AlertType = {
id: 'test',
@ -15,7 +16,6 @@ const alertType: AlertType = {
};
const createExecutionHandlerParams = {
log: jest.fn(),
executeAction: jest.fn(),
spaceId: 'default',
alertId: '1',
@ -23,6 +23,7 @@ const createExecutionHandlerParams = {
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
getBasePath: jest.fn().mockReturnValue(undefined),
alertType,
logger: loggingServiceMock.create().get(),
actions: [
{
id: '1',
@ -133,8 +134,7 @@ test(`logs an error when action group isn't part of actionGroups available for t
alertInstanceId: '2',
});
expect(result).toBeUndefined();
expect(createExecutionHandlerParams.log).toHaveBeenCalledWith(
['error', 'alerting'],
expect(createExecutionHandlerParams.logger.error).toHaveBeenCalledWith(
'Invalid action group "invalid-group" for alert "test".'
);
});

View file

@ -4,18 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AlertAction, State, Context, AlertType, Log } from '../types';
import { ActionsPlugin } from '../../../actions';
import { AlertAction, State, Context, AlertType } from '../types';
import { ActionsPluginStartContract } from '../shim';
import { Logger } from '../../../../../../src/core/server';
import { transformActionParams } from './transform_action_params';
interface CreateExecutionHandlerOptions {
alertId: string;
executeAction: ActionsPlugin['execute'];
executeAction: ActionsPluginStartContract['execute'];
actions: AlertAction[];
spaceId: string;
apiKey?: string;
alertType: AlertType;
log: Log;
logger: Logger;
}
interface ExecutionHandlerOptions {
@ -26,7 +27,7 @@ interface ExecutionHandlerOptions {
}
export function createExecutionHandler({
log,
logger,
alertId,
executeAction,
actions: alertActions,
@ -36,10 +37,7 @@ export function createExecutionHandler({
}: CreateExecutionHandlerOptions) {
return async ({ actionGroup, context, state, alertInstanceId }: ExecutionHandlerOptions) => {
if (!alertType.actionGroups.includes(actionGroup)) {
log(
['error', 'alerting'],
`Invalid action group "${actionGroup}" for alert "${alertType.id}".`
);
logger.error(`Invalid action group "${actionGroup}" for alert "${alertType.id}".`);
return;
}
const actions = alertActions

View file

@ -5,6 +5,7 @@
*/
export { AlertInstance } from './alert_instance';
export { getCreateTaskRunnerFunction } from './get_create_task_runner_function';
export { validateAlertTypeParams } from './validate_alert_type_params';
export { parseDuration, getDurationSchema } from './parse_duration';
export { AlertsClientFactory } from './alerts_client_factory';
export { TaskRunnerFactory } from './task_runner_factory';

View file

@ -8,11 +8,21 @@ import sinon from 'sinon';
import { schema } from '@kbn/config-schema';
import { AlertExecutorOptions } from '../types';
import { ConcreteTaskInstance } from '../../../task_manager';
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { getCreateTaskRunnerFunction } from './get_create_task_runner_function';
import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
import {
SavedObjectsClientMock,
loggingServiceMock,
} from '../../../../../../src/core/server/mocks';
const alertType = {
id: 'test',
name: 'My test alert',
actionGroups: ['default'],
executor: jest.fn(),
};
let fakeTimer: sinon.SinonFakeTimers;
let taskRunnerFactory: TaskRunnerFactory;
let mockedTaskInstance: ConcreteTaskInstance;
beforeAll(() => {
@ -35,6 +45,8 @@ beforeAll(() => {
},
ownerId: null,
};
taskRunnerFactory = new TaskRunnerFactory();
taskRunnerFactory.initialize(taskRunnerFactoryInitializerParams);
});
afterAll(() => fakeTimer.restore());
@ -47,17 +59,12 @@ const services = {
savedObjectsClient,
};
const getCreateTaskRunnerFunctionParams = {
const taskRunnerFactoryInitializerParams: jest.Mocked<TaskRunnerContext> = {
isSecurityEnabled: true,
getServices: jest.fn().mockReturnValue(services),
alertType: {
id: 'test',
name: 'My test alert',
actionGroups: ['default'],
executor: jest.fn(),
},
executeAction: jest.fn(),
encryptedSavedObjectsPlugin,
logger: loggingServiceMock.create().get(),
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
getBasePath: jest.fn().mockReturnValue(undefined),
};
@ -94,11 +101,28 @@ const mockedAlertTypeSavedObject = {
beforeEach(() => {
jest.resetAllMocks();
getCreateTaskRunnerFunctionParams.getServices.mockReturnValue(services);
taskRunnerFactoryInitializerParams.getServices.mockReturnValue(services);
});
test(`throws an error if factory isn't initialized`, () => {
const factory = new TaskRunnerFactory();
expect(() =>
factory.create(alertType, { taskInstance: mockedTaskInstance })
).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory not initialized"`);
});
test(`throws an error if factory is already initialized`, () => {
const factory = new TaskRunnerFactory();
factory.initialize(taskRunnerFactoryInitializerParams);
expect(() =>
factory.initialize(taskRunnerFactoryInitializerParams)
).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory already initialized"`);
});
test('successfully executes the task', async () => {
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
const taskRunner = taskRunnerFactory.create(alertType, {
taskInstance: mockedTaskInstance,
});
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
@ -108,8 +132,7 @@ test('successfully executes the task', async () => {
},
references: [],
});
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
const runnerResult = await runner.run();
const runnerResult = await taskRunner.run();
expect(runnerResult).toMatchInlineSnapshot(`
Object {
"runAt": 1970-01-01T00:00:10.000Z,
@ -120,8 +143,8 @@ test('successfully executes the task', async () => {
},
}
`);
expect(getCreateTaskRunnerFunctionParams.alertType.executor).toHaveBeenCalledTimes(1);
const call = getCreateTaskRunnerFunctionParams.alertType.executor.mock.calls[0][0];
expect(alertType.executor).toHaveBeenCalledTimes(1);
const call = alertType.executor.mock.calls[0][0];
expect(call.params).toMatchInlineSnapshot(`
Object {
"bar": true,
@ -135,12 +158,12 @@ test('successfully executes the task', async () => {
});
test('executeAction is called per alert instance that is scheduled', async () => {
getCreateTaskRunnerFunctionParams.alertType.executor.mockImplementation(
({ services: executorServices }: AlertExecutorOptions) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
}
);
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
alertType.executor.mockImplementation(({ services: executorServices }: AlertExecutorOptions) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
});
const taskRunner = taskRunnerFactory.create(alertType, {
taskInstance: mockedTaskInstance,
});
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
@ -150,10 +173,9 @@ test('executeAction is called per alert instance that is scheduled', async () =>
},
references: [],
});
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
await runner.run();
expect(getCreateTaskRunnerFunctionParams.executeAction).toHaveBeenCalledTimes(1);
expect(getCreateTaskRunnerFunctionParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(`
await taskRunner.run();
expect(taskRunnerFactoryInitializerParams.executeAction).toHaveBeenCalledTimes(1);
expect(taskRunnerFactoryInitializerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"apiKey": "MTIzOmFiYw==",
@ -168,22 +190,10 @@ test('executeAction is called per alert instance that is scheduled', async () =>
});
test('persists alertInstances passed in from state, only if they are scheduled for execution', async () => {
getCreateTaskRunnerFunctionParams.alertType.executor.mockImplementation(
({ services: executorServices }: AlertExecutorOptions) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
}
);
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [],
alertType.executor.mockImplementation(({ services: executorServices }: AlertExecutorOptions) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
});
const runner = createTaskRunner({
const taskRunner = taskRunnerFactory.create(alertType, {
taskInstance: {
...mockedTaskInstance,
state: {
@ -195,7 +205,16 @@ test('persists alertInstances passed in from state, only if they are scheduled f
},
},
});
const runnerResult = await runner.run();
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [],
});
const runnerResult = await taskRunner.run();
expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(`
Object {
"1": Object {
@ -214,17 +233,17 @@ test('persists alertInstances passed in from state, only if they are scheduled f
});
test('validates params before executing the alert type', async () => {
const createTaskRunner = getCreateTaskRunnerFunction({
...getCreateTaskRunnerFunctionParams,
alertType: {
...getCreateTaskRunnerFunctionParams.alertType,
const taskRunner = taskRunnerFactory.create(
{
...alertType,
validate: {
params: schema.object({
param1: schema.string(),
}),
},
},
});
{ taskInstance: mockedTaskInstance }
);
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
@ -234,14 +253,15 @@ test('validates params before executing the alert type', async () => {
},
references: [],
});
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
await expect(runner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(taskRunner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
`"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"`
);
});
test('throws error if reference not found', async () => {
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
const taskRunner = taskRunnerFactory.create(alertType, {
taskInstance: mockedTaskInstance,
});
savedObjectsClient.get.mockResolvedValueOnce({
...mockedAlertTypeSavedObject,
references: [],
@ -254,14 +274,15 @@ test('throws error if reference not found', async () => {
},
references: [],
});
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
await expect(runner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(taskRunner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
`"Action reference \\"action_0\\" not found in alert id: 1"`
);
});
test('uses API key when provided', async () => {
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
const taskRunner = taskRunnerFactory.create(alertType, {
taskInstance: mockedTaskInstance,
});
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
@ -271,23 +292,36 @@ test('uses API key when provided', async () => {
},
references: [],
});
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
await runner.run();
expect(getCreateTaskRunnerFunctionParams.getServices).toHaveBeenCalledWith({
await taskRunner.run();
expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({
getBasePath: expect.anything(),
headers: {
// base64 encoded "123:abc"
authorization: 'ApiKey MTIzOmFiYw==',
},
path: '/',
route: { settings: {} },
url: {
href: '/',
},
raw: {
req: {
url: '/',
},
},
});
});
test(`doesn't use API key when not provided`, async () => {
const createTaskRunner = getCreateTaskRunnerFunction({
...getCreateTaskRunnerFunctionParams,
const factory = new TaskRunnerFactory();
factory.initialize({
...taskRunnerFactoryInitializerParams,
isSecurityEnabled: false,
});
const taskRunner = factory.create(alertType, {
taskInstance: mockedTaskInstance,
});
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '1',
@ -295,12 +329,21 @@ test(`doesn't use API key when not provided`, async () => {
attributes: {},
references: [],
});
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
await runner.run();
await taskRunner.run();
expect(getCreateTaskRunnerFunctionParams.getServices).toHaveBeenCalledWith({
expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({
getBasePath: expect.anything(),
headers: {},
path: '/',
route: { settings: {} },
url: {
href: '/',
},
raw: {
req: {
url: '/',
},
},
});
});

View file

@ -4,14 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ActionsPlugin } from '../../../actions';
import { ConcreteTaskInstance } from '../../../task_manager';
import { Logger } from '../../../../../../src/core/server';
import { RunContext } from '../../../task_manager';
import { createExecutionHandler } from './create_execution_handler';
import { createAlertInstanceFactory } from './create_alert_instance_factory';
import { AlertInstance } from './alert_instance';
import { getNextRunAt } from './get_next_run_at';
import { validateAlertTypeParams } from './validate_alert_type_params';
import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects';
import { ActionsPluginStartContract, EncryptedSavedObjectsStartContract } from '../shim';
import {
AlertType,
AlertServices,
@ -21,32 +21,45 @@ import {
SpaceIdToNamespaceFunction,
} from '../types';
export interface CreateTaskRunnerFunctionOptions {
export interface TaskRunnerContext {
logger: Logger;
isSecurityEnabled: boolean;
getServices: GetServicesFunction;
alertType: AlertType;
executeAction: ActionsPlugin['execute'];
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
executeAction: ActionsPluginStartContract['execute'];
encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract;
spaceIdToNamespace: SpaceIdToNamespaceFunction;
getBasePath: GetBasePathFunction;
}
interface TaskRunnerOptions {
taskInstance: ConcreteTaskInstance;
}
export class TaskRunnerFactory {
private isInitialized = false;
private taskRunnerContext?: TaskRunnerContext;
public initialize(taskRunnerContext: TaskRunnerContext) {
if (this.isInitialized) {
throw new Error('TaskRunnerFactory already initialized');
}
this.isInitialized = true;
this.taskRunnerContext = taskRunnerContext;
}
public create(alertType: AlertType, { taskInstance }: RunContext) {
if (!this.isInitialized) {
throw new Error('TaskRunnerFactory not initialized');
}
const {
logger,
isSecurityEnabled,
getServices,
executeAction,
encryptedSavedObjectsPlugin,
spaceIdToNamespace,
getBasePath,
} = this.taskRunnerContext!;
export function getCreateTaskRunnerFunction({
getServices,
alertType,
executeAction,
encryptedSavedObjectsPlugin,
spaceIdToNamespace,
getBasePath,
isSecurityEnabled,
}: CreateTaskRunnerFunctionOptions) {
return ({ taskInstance }: TaskRunnerOptions) => {
return {
run: async () => {
async run() {
const { alertId, spaceId } = taskInstance.params;
const requestHeaders: Record<string, string> = {};
const namespace = spaceIdToNamespace(spaceId);
@ -69,6 +82,16 @@ export function getCreateTaskRunnerFunction({
const fakeRequest = {
headers: requestHeaders,
getBasePath: () => getBasePath(spaceId),
path: '/',
route: { settings: {} },
url: {
href: '/',
},
raw: {
req: {
url: '/',
},
},
};
const services = getServices(fakeRequest);
@ -97,7 +120,7 @@ export function getCreateTaskRunnerFunction({
const executionHandler = createExecutionHandler({
alertId,
log: services.log,
logger,
executeAction,
apiKey,
actions: actionsWithIds,
@ -154,5 +177,5 @@ export function getCreateTaskRunnerFunction({
};
},
};
};
}
}

View file

@ -0,0 +1,168 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Hapi from 'hapi';
import { first } from 'rxjs/operators';
import { Services } from './types';
import { AlertsClient } from './alerts_client';
import { AlertTypeRegistry } from './alert_type_registry';
import { AlertsClientFactory, TaskRunnerFactory } from './lib';
import { IClusterClient, KibanaRequest, Logger } from '../../../../../src/core/server';
import {
AlertingPluginInitializerContext,
AlertingCoreSetup,
AlertingCoreStart,
AlertingPluginsSetup,
AlertingPluginsStart,
} from './shim';
import {
createAlertRoute,
deleteAlertRoute,
findAlertRoute,
getAlertRoute,
listAlertTypesRoute,
updateAlertRoute,
enableAlertRoute,
disableAlertRoute,
updateApiKeyRoute,
muteAllAlertRoute,
unmuteAllAlertRoute,
muteAlertInstanceRoute,
unmuteAlertInstanceRoute,
} from './routes';
export interface PluginSetupContract {
registerType: AlertTypeRegistry['register'];
}
export interface PluginStartContract {
listTypes: AlertTypeRegistry['list'];
getAlertsClientWithRequest(request: Hapi.Request): AlertsClient;
}
export class Plugin {
private readonly logger: Logger;
private alertTypeRegistry?: AlertTypeRegistry;
private readonly taskRunnerFactory: TaskRunnerFactory;
private adminClient?: IClusterClient;
private serverBasePath?: string;
constructor(initializerContext: AlertingPluginInitializerContext) {
this.logger = initializerContext.logger.get('plugins', 'alerting');
this.taskRunnerFactory = new TaskRunnerFactory();
}
public async setup(
core: AlertingCoreSetup,
plugins: AlertingPluginsSetup
): Promise<PluginSetupContract> {
this.adminClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise();
plugins.xpack_main.registerFeature({
id: 'alerting',
name: 'Alerting',
app: ['alerting', 'kibana'],
privileges: {
all: {
savedObject: {
all: ['alert'],
read: [],
},
ui: [],
api: ['alerting-read', 'alerting-all'],
},
read: {
savedObject: {
all: [],
read: ['alert'],
},
ui: [],
api: ['alerting-read'],
},
},
});
// Encrypted attributes
plugins.encrypted_saved_objects.registerType({
type: 'alert',
attributesToEncrypt: new Set(['apiKey']),
attributesToExcludeFromAAD: new Set([
'scheduledTaskId',
'muted',
'mutedInstanceIds',
'updatedBy',
]),
});
const alertTypeRegistry = new AlertTypeRegistry({
taskManager: plugins.task_manager,
taskRunnerFactory: this.taskRunnerFactory,
});
this.alertTypeRegistry = alertTypeRegistry;
this.serverBasePath = core.http.basePath.serverBasePath;
// Register routes
core.http.route(createAlertRoute);
core.http.route(deleteAlertRoute);
core.http.route(findAlertRoute);
core.http.route(getAlertRoute);
core.http.route(listAlertTypesRoute);
core.http.route(updateAlertRoute);
core.http.route(enableAlertRoute);
core.http.route(disableAlertRoute);
core.http.route(updateApiKeyRoute);
core.http.route(muteAllAlertRoute);
core.http.route(unmuteAllAlertRoute);
core.http.route(muteAlertInstanceRoute);
core.http.route(unmuteAlertInstanceRoute);
return {
registerType: alertTypeRegistry.register.bind(alertTypeRegistry),
};
}
public start(core: AlertingCoreStart, plugins: AlertingPluginsStart): PluginStartContract {
const { adminClient, serverBasePath } = this;
const alertsClientFactory = new AlertsClientFactory({
alertTypeRegistry: this.alertTypeRegistry!,
logger: this.logger,
taskManager: plugins.task_manager,
securityPluginSetup: plugins.security,
getSpaceId(request: Hapi.Request) {
const spacesPlugin = plugins.spaces();
return spacesPlugin ? spacesPlugin.getSpaceId(request) : undefined;
},
});
this.taskRunnerFactory.initialize({
logger: this.logger,
isSecurityEnabled: !!plugins.security,
getServices(request: Hapi.Request): Services {
return {
callCluster: (...args) =>
adminClient!.asScoped(KibanaRequest.from(request)).callAsCurrentUser(...args),
savedObjectsClient: core.savedObjects.getScopedSavedObjectsClient(request),
};
},
executeAction: plugins.actions.execute,
encryptedSavedObjectsPlugin: plugins.encrypted_saved_objects,
spaceIdToNamespace(spaceId?: string): string | undefined {
const spacesPlugin = plugins.spaces();
return spacesPlugin && spaceId ? spacesPlugin.spaceIdToNamespace(spaceId) : undefined;
},
getBasePath(spaceId?: string): string {
const spacesPlugin = plugins.spaces();
return spacesPlugin && spaceId ? spacesPlugin.getBasePath(spaceId) : serverBasePath!;
},
});
return {
listTypes: this.alertTypeRegistry!.list.bind(this.alertTypeRegistry!),
getAlertsClientWithRequest: (request: Hapi.Request) =>
alertsClientFactory!.create(KibanaRequest.from(request), request),
};
}
}

View file

@ -34,8 +34,14 @@ export function createMockServer(config: Record<string, any> = defaultConfig) {
server.register({
name: 'alerting',
register(pluginServer: Hapi.Server) {
pluginServer.expose('registerType', alertTypeRegistry.register);
pluginServer.expose('listTypes', alertTypeRegistry.list);
pluginServer.expose({
setup: {
registerType: alertTypeRegistry.register,
},
start: {
listTypes: alertTypeRegistry.list,
},
});
},
});

View file

@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server';
import { createAlertRoute } from './create';
const { server, alertsClient } = createMockServer();
createAlertRoute(server);
server.route(createAlertRoute);
const mockedAlert = {
alertTypeId: '1',

View file

@ -20,39 +20,37 @@ interface ScheduleRequest extends Hapi.Request {
};
}
export function createAlertRoute(server: Hapi.Server) {
server.route({
method: 'POST',
path: '/api/alert',
options: {
tags: ['access:alerting-all'],
validate: {
options: {
abortEarly: false,
},
payload: Joi.object()
.keys({
enabled: Joi.boolean().default(true),
alertTypeId: Joi.string().required(),
throttle: getDurationSchema().default(null),
interval: getDurationSchema().required(),
alertTypeParams: Joi.object().required(),
actions: Joi.array()
.items(
Joi.object().keys({
group: Joi.string().required(),
id: Joi.string().required(),
params: Joi.object().required(),
})
)
.required(),
})
.required(),
export const createAlertRoute = {
method: 'POST',
path: '/api/alert',
options: {
tags: ['access:alerting-all'],
validate: {
options: {
abortEarly: false,
},
payload: Joi.object()
.keys({
enabled: Joi.boolean().default(true),
alertTypeId: Joi.string().required(),
throttle: getDurationSchema().default(null),
interval: getDurationSchema().required(),
alertTypeParams: Joi.object().required(),
actions: Joi.array()
.items(
Joi.object().keys({
group: Joi.string().required(),
id: Joi.string().required(),
params: Joi.object().required(),
})
)
.required(),
})
.required(),
},
async handler(request: ScheduleRequest) {
const alertsClient = request.getAlertsClient!();
return await alertsClient.create({ data: request.payload });
},
});
}
},
async handler(request: ScheduleRequest) {
const alertsClient = request.getAlertsClient!();
return await alertsClient.create({ data: request.payload });
},
};

View file

@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server';
import { deleteAlertRoute } from './delete';
const { server, alertsClient } = createMockServer();
deleteAlertRoute(server);
server.route(deleteAlertRoute);
beforeEach(() => jest.resetAllMocks());

View file

@ -13,25 +13,23 @@ interface DeleteRequest extends Hapi.Request {
};
}
export function deleteAlertRoute(server: Hapi.Server) {
server.route({
method: 'DELETE',
path: '/api/alert/{id}',
options: {
tags: ['access:alerting-all'],
validate: {
params: Joi.object()
.keys({
id: Joi.string().required(),
})
.required(),
},
export const deleteAlertRoute = {
method: 'DELETE',
path: '/api/alert/{id}',
config: {
tags: ['access:alerting-all'],
validate: {
params: Joi.object()
.keys({
id: Joi.string().required(),
})
.required(),
},
async handler(request: DeleteRequest, h: Hapi.ResponseToolkit) {
const { id } = request.params;
const alertsClient = request.getAlertsClient!();
await alertsClient.delete({ id });
return h.response().code(204);
},
});
}
},
async handler(request: DeleteRequest, h: Hapi.ResponseToolkit) {
const { id } = request.params;
const alertsClient = request.getAlertsClient!();
await alertsClient.delete({ id });
return h.response().code(204);
},
};

View file

@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server';
import { disableAlertRoute } from './disable';
const { server, alertsClient } = createMockServer();
disableAlertRoute(server);
server.route(disableAlertRoute);
test('disables an alert', async () => {
const request = {

View file

@ -6,20 +6,18 @@
import Hapi from 'hapi';
export function disableAlertRoute(server: Hapi.Server) {
server.route({
method: 'POST',
path: '/api/alert/{id}/_disable',
options: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
export const disableAlertRoute = {
method: 'POST',
path: '/api/alert/{id}/_disable',
config: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.disable({ id: request.params.id });
return h.response();
},
});
}
},
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.disable({ id: request.params.id });
return h.response();
},
};

View file

@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server';
import { enableAlertRoute } from './enable';
const { server, alertsClient } = createMockServer();
enableAlertRoute(server);
server.route(enableAlertRoute);
test('enables an alert', async () => {
const request = {

View file

@ -6,20 +6,18 @@
import Hapi from 'hapi';
export function enableAlertRoute(server: Hapi.Server) {
server.route({
method: 'POST',
path: '/api/alert/{id}/_enable',
options: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
export const enableAlertRoute = {
method: 'POST',
path: '/api/alert/{id}/_enable',
config: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.enable({ id: request.params.id });
return h.response();
},
});
}
},
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.enable({ id: request.params.id });
return h.response();
},
};

View file

@ -5,10 +5,10 @@
*/
import { createMockServer } from './_mock_server';
import { findRoute } from './find';
import { findAlertRoute } from './find';
const { server, alertsClient } = createMockServer();
findRoute(server);
server.route(findAlertRoute);
beforeEach(() => jest.resetAllMocks());

View file

@ -25,59 +25,57 @@ interface FindRequest extends WithoutQueryAndParams<Hapi.Request> {
};
}
export function findRoute(server: Hapi.Server) {
server.route({
method: 'GET',
path: '/api/alert/_find',
options: {
tags: ['access:alerting-read'],
validate: {
query: Joi.object()
.keys({
per_page: Joi.number()
.min(0)
.default(20),
page: Joi.number()
.min(1)
.default(1),
search: Joi.string()
.allow('')
.optional(),
default_search_operator: Joi.string()
.valid('OR', 'AND')
.default('OR'),
search_fields: Joi.array()
.items(Joi.string())
.single(),
sort_field: Joi.string(),
has_reference: Joi.object()
.keys({
type: Joi.string().required(),
id: Joi.string().required(),
})
.optional(),
fields: Joi.array()
.items(Joi.string())
.single(),
})
.default(),
export const findAlertRoute = {
method: 'GET',
path: '/api/alert/_find',
config: {
tags: ['access:alerting-read'],
validate: {
query: Joi.object()
.keys({
per_page: Joi.number()
.min(0)
.default(20),
page: Joi.number()
.min(1)
.default(1),
search: Joi.string()
.allow('')
.optional(),
default_search_operator: Joi.string()
.valid('OR', 'AND')
.default('OR'),
search_fields: Joi.array()
.items(Joi.string())
.single(),
sort_field: Joi.string(),
has_reference: Joi.object()
.keys({
type: Joi.string().required(),
id: Joi.string().required(),
})
.optional(),
fields: Joi.array()
.items(Joi.string())
.single(),
})
.default(),
},
},
async handler(request: FindRequest) {
const { query } = request;
const alertsClient = request.getAlertsClient!();
return await alertsClient.find({
options: {
perPage: query.per_page,
page: query.page,
search: query.search,
defaultSearchOperator: query.default_search_operator,
searchFields: query.search_fields,
sortField: query.sort_field,
hasReference: query.has_reference,
fields: query.fields,
},
},
async handler(request: FindRequest) {
const { query } = request;
const alertsClient = request.getAlertsClient!();
return await alertsClient.find({
options: {
perPage: query.per_page,
page: query.page,
search: query.search,
defaultSearchOperator: query.default_search_operator,
searchFields: query.search_fields,
sortField: query.sort_field,
hasReference: query.has_reference,
fields: query.fields,
},
});
},
});
}
});
},
};

View file

@ -5,10 +5,10 @@
*/
import { createMockServer } from './_mock_server';
import { getRoute } from './get';
import { getAlertRoute } from './get';
const { server, alertsClient } = createMockServer();
getRoute(server);
server.route(getAlertRoute);
const mockedAlert = {
id: '1',

View file

@ -13,24 +13,22 @@ interface GetRequest extends Hapi.Request {
};
}
export function getRoute(server: Hapi.Server) {
server.route({
method: 'GET',
path: `/api/alert/{id}`,
options: {
tags: ['access:alerting-read'],
validate: {
params: Joi.object()
.keys({
id: Joi.string().required(),
})
.required(),
},
export const getAlertRoute = {
method: 'GET',
path: `/api/alert/{id}`,
options: {
tags: ['access:alerting-read'],
validate: {
params: Joi.object()
.keys({
id: Joi.string().required(),
})
.required(),
},
async handler(request: GetRequest) {
const { id } = request.params;
const alertsClient = request.getAlertsClient!();
return await alertsClient.get({ id });
},
});
}
},
async handler(request: GetRequest) {
const { id } = request.params;
const alertsClient = request.getAlertsClient!();
return await alertsClient.get({ id });
},
};

View file

@ -6,8 +6,8 @@
export { createAlertRoute } from './create';
export { deleteAlertRoute } from './delete';
export { findRoute } from './find';
export { getRoute } from './get';
export { findAlertRoute } from './find';
export { getAlertRoute } from './get';
export { listAlertTypesRoute } from './list_alert_types';
export { updateAlertRoute } from './update';
export { enableAlertRoute } from './enable';

View file

@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server';
import { listAlertTypesRoute } from './list_alert_types';
const { server, alertTypeRegistry } = createMockServer();
listAlertTypesRoute(server);
server.route(listAlertTypesRoute);
beforeEach(() => jest.resetAllMocks());

View file

@ -6,15 +6,13 @@
import Hapi from 'hapi';
export function listAlertTypesRoute(server: Hapi.Server) {
server.route({
method: 'GET',
path: `/api/alert/types`,
options: {
tags: ['access:alerting-read'],
},
async handler(request: Hapi.Request) {
return request.server.plugins.alerting!.listTypes();
},
});
}
export const listAlertTypesRoute = {
method: 'GET',
path: `/api/alert/types`,
config: {
tags: ['access:alerting-read'],
},
async handler(request: Hapi.Request) {
return request.server.plugins.alerting!.start.listTypes();
},
};

View file

@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server';
import { muteAllAlertRoute } from './mute_all';
const { server, alertsClient } = createMockServer();
muteAllAlertRoute(server);
server.route(muteAllAlertRoute);
test('mutes an alert', async () => {
const request = {

View file

@ -12,20 +12,18 @@ interface MuteAllRequest extends Hapi.Request {
};
}
export function muteAllAlertRoute(server: Hapi.Server) {
server.route({
method: 'POST',
path: '/api/alert/{id}/_mute_all',
options: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
export const muteAllAlertRoute = {
method: 'POST',
path: '/api/alert/{id}/_mute_all',
config: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
async handler(request: MuteAllRequest, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.muteAll(request.params);
return h.response();
},
});
}
},
async handler(request: MuteAllRequest, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.muteAll(request.params);
return h.response();
},
};

View file

@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server';
import { muteAlertInstanceRoute } from './mute_instance';
const { server, alertsClient } = createMockServer();
muteAlertInstanceRoute(server);
server.route(muteAlertInstanceRoute);
test('mutes an alert instance', async () => {
const request = {

View file

@ -13,20 +13,18 @@ interface MuteInstanceRequest extends Hapi.Request {
};
}
export function muteAlertInstanceRoute(server: Hapi.Server) {
server.route({
method: 'POST',
path: '/api/alert/{alertId}/alert_instance/{alertInstanceId}/_mute',
options: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
export const muteAlertInstanceRoute = {
method: 'POST',
path: '/api/alert/{alertId}/alert_instance/{alertInstanceId}/_mute',
config: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
async handler(request: MuteInstanceRequest, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.muteInstance(request.params);
return h.response();
},
});
}
},
async handler(request: MuteInstanceRequest, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.muteInstance(request.params);
return h.response();
},
};

View file

@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server';
import { unmuteAllAlertRoute } from './unmute_all';
const { server, alertsClient } = createMockServer();
unmuteAllAlertRoute(server);
server.route(unmuteAllAlertRoute);
test('unmutes an alert', async () => {
const request = {

View file

@ -12,20 +12,18 @@ interface UnmuteAllRequest extends Hapi.Request {
};
}
export function unmuteAllAlertRoute(server: Hapi.Server) {
server.route({
method: 'POST',
path: '/api/alert/{id}/_unmute_all',
options: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
export const unmuteAllAlertRoute = {
method: 'POST',
path: '/api/alert/{id}/_unmute_all',
config: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
async handler(request: UnmuteAllRequest, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.unmuteAll(request.params);
return h.response();
},
});
}
},
async handler(request: UnmuteAllRequest, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.unmuteAll(request.params);
return h.response();
},
};

View file

@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server';
import { unmuteAlertInstanceRoute } from './unmute_instance';
const { server, alertsClient } = createMockServer();
unmuteAlertInstanceRoute(server);
server.route(unmuteAlertInstanceRoute);
test('unmutes an alert instance', async () => {
const request = {

View file

@ -13,20 +13,18 @@ interface UnmuteInstanceRequest extends Hapi.Request {
};
}
export function unmuteAlertInstanceRoute(server: Hapi.Server) {
server.route({
method: 'POST',
path: '/api/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute',
options: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
export const unmuteAlertInstanceRoute = {
method: 'POST',
path: '/api/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute',
config: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
async handler(request: UnmuteInstanceRequest, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.unmuteInstance(request.params);
return h.response();
},
});
}
},
async handler(request: UnmuteInstanceRequest, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.unmuteInstance(request.params);
return h.response();
},
};

View file

@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server';
import { updateAlertRoute } from './update';
const { server, alertsClient } = createMockServer();
updateAlertRoute(server);
server.route(updateAlertRoute);
beforeEach(() => jest.resetAllMocks());

View file

@ -22,40 +22,38 @@ interface UpdateRequest extends Hapi.Request {
};
}
export function updateAlertRoute(server: Hapi.Server) {
server.route({
method: 'PUT',
path: '/api/alert/{id}',
options: {
tags: ['access:alerting-all'],
validate: {
options: {
abortEarly: false,
},
payload: Joi.object()
.keys({
throttle: getDurationSchema()
.required()
.allow(null),
interval: getDurationSchema().required(),
alertTypeParams: Joi.object().required(),
actions: Joi.array()
.items(
Joi.object().keys({
group: Joi.string().required(),
id: Joi.string().required(),
params: Joi.object().required(),
})
)
.required(),
})
.required(),
export const updateAlertRoute = {
method: 'PUT',
path: '/api/alert/{id}',
options: {
tags: ['access:alerting-all'],
validate: {
options: {
abortEarly: false,
},
payload: Joi.object()
.keys({
throttle: getDurationSchema()
.required()
.allow(null),
interval: getDurationSchema().required(),
alertTypeParams: Joi.object().required(),
actions: Joi.array()
.items(
Joi.object().keys({
group: Joi.string().required(),
id: Joi.string().required(),
params: Joi.object().required(),
})
)
.required(),
})
.required(),
},
async handler(request: UpdateRequest) {
const { id } = request.params;
const alertsClient = request.getAlertsClient!();
return await alertsClient.update({ id, data: request.payload });
},
});
}
},
async handler(request: UpdateRequest) {
const { id } = request.params;
const alertsClient = request.getAlertsClient!();
return await alertsClient.update({ id, data: request.payload });
},
};

View file

@ -8,7 +8,7 @@ import { createMockServer } from './_mock_server';
import { updateApiKeyRoute } from './update_api_key';
const { server, alertsClient } = createMockServer();
updateApiKeyRoute(server);
server.route(updateApiKeyRoute);
test('updates api key for an alert', async () => {
const request = {

View file

@ -6,20 +6,18 @@
import Hapi from 'hapi';
export function updateApiKeyRoute(server: Hapi.Server) {
server.route({
method: 'POST',
path: '/api/alert/{id}/_update_api_key',
options: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
export const updateApiKeyRoute = {
method: 'POST',
path: '/api/alert/{id}/_update_api_key',
config: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.updateApiKey({ id: request.params.id });
return h.response();
},
});
}
},
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.updateApiKey({ id: request.params.id });
return h.response();
},
};

View file

@ -0,0 +1,143 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Hapi from 'hapi';
import { Legacy } from 'kibana';
import { SpacesPlugin as SpacesPluginStartContract } from '../../spaces';
import { ActionsPlugin } from '../../actions';
import { TaskManager } from '../../task_manager';
import { XPackMainPlugin } from '../../xpack_main/xpack_main';
import KbnServer from '../../../../../src/legacy/server/kbn_server';
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server';
import {
CoreSetup,
LoggerFactory,
SavedObjectsLegacyService,
} from '../../../../../src/core/server';
// Extend PluginProperties to indicate which plugins are guaranteed to exist
// due to being marked as dependencies
interface Plugins extends Hapi.PluginProperties {
actions: ActionsPlugin;
task_manager: TaskManager;
encrypted_saved_objects: EncryptedSavedObjectsPlugin;
}
export interface Server extends Legacy.Server {
plugins: Plugins;
}
/**
* Shim what we're thinking setup and start contracts will look like
*/
export type ActionsPluginSetupContract = Pick<ActionsPlugin, 'registerType'>;
export type ActionsPluginStartContract = Pick<ActionsPlugin, 'listTypes' | 'execute'>;
export type TaskManagerStartContract = Pick<TaskManager, 'schedule' | 'fetch' | 'remove'>;
export type SecurityPluginSetupContract = Pick<SecurityPlugin, 'config' | 'registerLegacyAPI'>;
export type SecurityPluginStartContract = Pick<SecurityPlugin, 'authc'>;
export type EncryptedSavedObjectsSetupContract = Pick<EncryptedSavedObjectsPlugin, 'registerType'>;
export type XPackMainPluginSetupContract = Pick<XPackMainPlugin, 'registerFeature'>;
export type TaskManagerSetupContract = Pick<
TaskManager,
'addMiddleware' | 'registerTaskDefinitions'
>;
export type EncryptedSavedObjectsStartContract = Pick<
EncryptedSavedObjectsPlugin,
'isEncryptionError' | 'getDecryptedAsInternalUser'
>;
/**
* New platform interfaces
*/
export interface AlertingPluginInitializerContext {
logger: LoggerFactory;
}
export interface AlertingCoreSetup {
elasticsearch: CoreSetup['elasticsearch'];
http: {
route: (route: Hapi.ServerRoute) => void;
basePath: {
serverBasePath: string;
};
};
}
export interface AlertingCoreStart {
savedObjects: SavedObjectsLegacyService;
}
export interface AlertingPluginsSetup {
security?: SecurityPluginSetupContract;
task_manager: TaskManagerSetupContract;
actions: ActionsPluginSetupContract;
xpack_main: XPackMainPluginSetupContract;
encrypted_saved_objects: EncryptedSavedObjectsSetupContract;
}
export interface AlertingPluginsStart {
actions: ActionsPluginStartContract;
security?: SecurityPluginStartContract;
spaces: () => SpacesPluginStartContract | undefined;
encrypted_saved_objects: EncryptedSavedObjectsStartContract;
task_manager: TaskManagerStartContract;
}
/**
* Shim
*
* @param server Hapi server instance
*/
export function shim(
server: Server
): {
initializerContext: AlertingPluginInitializerContext;
coreSetup: AlertingCoreSetup;
coreStart: AlertingCoreStart;
pluginsSetup: AlertingPluginsSetup;
pluginsStart: AlertingPluginsStart;
} {
const newPlatform = ((server as unknown) as KbnServer).newPlatform;
const initializerContext: AlertingPluginInitializerContext = {
logger: newPlatform.coreContext.logger,
};
const coreSetup: AlertingCoreSetup = {
elasticsearch: newPlatform.setup.core.elasticsearch,
http: {
route: server.route.bind(server),
basePath: newPlatform.setup.core.http.basePath,
},
};
const coreStart: AlertingCoreStart = {
savedObjects: server.savedObjects,
};
const pluginsSetup: AlertingPluginsSetup = {
security: newPlatform.setup.plugins.security as SecurityPluginSetupContract | undefined,
task_manager: server.plugins.task_manager,
actions: server.plugins.actions,
xpack_main: server.plugins.xpack_main,
encrypted_saved_objects: server.plugins.encrypted_saved_objects,
};
const pluginsStart: AlertingPluginsStart = {
security: newPlatform.setup.plugins.security as SecurityPluginStartContract | undefined,
actions: server.plugins.actions,
// TODO: Currently a function because it's an optional dependency that
// initializes after this function is called
spaces: () => server.plugins.spaces,
encrypted_saved_objects: server.plugins.encrypted_saved_objects,
task_manager: server.plugins.task_manager,
};
return {
initializerContext,
coreSetup,
coreStart,
pluginsSetup,
pluginsStart,
};
}

View file

@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectAttributes, SavedObjectsClientContract } from 'src/core/server';
import { AlertInstance } from './lib';
import { AlertTypeRegistry } from './alert_type_registry';
import { PluginSetupContract, PluginStartContract } from './plugin';
import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../../src/core/server';
export type State = Record<string, any>;
export type Context = Record<string, any>;
@ -15,14 +16,7 @@ export type GetServicesFunction = (request: any) => Services;
export type GetBasePathFunction = (spaceId?: string) => string;
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
export type Log = (
tags: string | string[],
data?: string | object | (() => any),
timestamp?: number
) => void;
export interface Services {
log: Log;
callCluster(path: string, opts: any): Promise<any>;
savedObjectsClient: SavedObjectsClientContract;
}
@ -96,8 +90,8 @@ export interface RawAlert extends SavedObjectAttributes {
}
export interface AlertingPlugin {
registerType: AlertTypeRegistry['register'];
listTypes: AlertTypeRegistry['list'];
setup: PluginSetupContract;
start: PluginStartContract;
}
export type AlertTypeRegistry = PublicMethodsOf<AlertTypeRegistry>;

View file

@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n';
import { resolve } from 'path';
import { Server } from 'hapi';
import KbnServer from '../../../../src/legacy/server/kbn_server';
import { initServerWithKibana } from './server/kibana.index';
import { savedObjectMappings } from './server/saved_objects';
@ -122,8 +123,11 @@ export function siem(kibana: any) {
mappings: savedObjectMappings,
},
init(server: Server) {
const newPlatform = ((server as unknown) as KbnServer).newPlatform;
if (server.plugins.alerting != null) {
server.plugins.alerting.registerType(signalsAlertType);
server.plugins.alerting.setup.registerType(
signalsAlertType({ logger: newPlatform.coreContext.logger.get('plugins', APP_ID) })
);
}
server.injectUiAppVars('siem', async () => server.getInjectedUiAppVars('kibana'));
initServerWithKibana(server);

View file

@ -6,6 +6,7 @@
import { schema } from '@kbn/config-schema';
import { SIGNALS_ID } from '../../../../common/constants';
import { Logger } from '../../../../../../../../src/core/server';
import { AlertType, AlertExecutorOptions } from '../../../../../alerting';
// TODO: Remove this for the build_events_query call eventually
@ -15,85 +16,87 @@ import { buildEventsReIndex } from './build_events_reindex';
// once scrolling and other things are done with it.
// import { buildEventsQuery } from './build_events_query';
export const signalsAlertType: AlertType = {
id: SIGNALS_ID,
name: 'SIEM Signals',
actionGroups: ['default'],
validate: {
params: schema.object({
description: schema.string(),
from: schema.string(),
filter: schema.maybe(schema.object({}, { allowUnknowns: true })),
id: schema.number(),
index: schema.arrayOf(schema.string()),
kql: schema.maybe(schema.string({ defaultValue: undefined })),
maxSignals: schema.number({ defaultValue: 100 }),
name: schema.string(),
severity: schema.number(),
to: schema.string(),
type: schema.string(),
references: schema.arrayOf(schema.string(), { defaultValue: [] }),
}),
},
// TODO: Type the params as it is all filled with any
async executor({ services, params, state }: AlertExecutorOptions) {
const instance = services.alertInstanceFactory('siem-signals');
export const signalsAlertType = ({ logger }: { logger: Logger }): AlertType => {
return {
id: SIGNALS_ID,
name: 'SIEM Signals',
actionGroups: ['default'],
validate: {
params: schema.object({
description: schema.string(),
from: schema.string(),
filter: schema.maybe(schema.object({}, { allowUnknowns: true })),
id: schema.number(),
index: schema.arrayOf(schema.string()),
kql: schema.maybe(schema.string({ defaultValue: undefined })),
maxSignals: schema.number({ defaultValue: 100 }),
name: schema.string(),
severity: schema.number(),
to: schema.string(),
type: schema.string(),
references: schema.arrayOf(schema.string(), { defaultValue: [] }),
}),
},
// TODO: Type the params as it is all filled with any
async executor({ services, params, state }: AlertExecutorOptions) {
const instance = services.alertInstanceFactory('siem-signals');
// TODO: Comment this in eventually and use the buildEventsQuery()
// for scrolling and other fun stuff instead of using the buildEventsReIndex()
// const query = buildEventsQuery();
// TODO: Comment this in eventually and use the buildEventsQuery()
// for scrolling and other fun stuff instead of using the buildEventsReIndex()
// const query = buildEventsQuery();
const {
description,
filter,
from,
id,
index,
kql,
maxSignals,
name,
references,
severity,
to,
type,
} = params;
const reIndex = buildEventsReIndex({
index,
from,
kql,
to,
// TODO: Change this out once we have solved
// https://github.com/elastic/kibana/issues/47002
signalsIndex: process.env.SIGNALS_INDEX || '.siem-signals-10-01-2019',
severity,
description,
name,
timeDetected: Date.now(),
filter,
maxDocs: maxSignals,
ruleRevision: 1,
id,
type,
references,
});
const {
description,
filter,
from,
id,
index,
kql,
maxSignals,
name,
references,
severity,
to,
type,
} = params;
const reIndex = buildEventsReIndex({
index,
from,
kql,
to,
// TODO: Change this out once we have solved
// https://github.com/elastic/kibana/issues/47002
signalsIndex: process.env.SIGNALS_INDEX || '.siem-signals-10-01-2019',
severity,
description,
name,
timeDetected: Date.now(),
filter,
maxDocs: maxSignals,
ruleRevision: 1,
id,
type,
references,
});
try {
services.log(['info', 'SIEM'], 'Starting SIEM signal job');
try {
logger.info('Starting SIEM signal job');
// TODO: Comment this in eventually and use this for manual insertion of the
// signals instead of the ReIndex() api
// const result = await services.callCluster('search', query);
const result = await services.callCluster('reindex', reIndex);
// TODO: Comment this in eventually and use this for manual insertion of the
// signals instead of the ReIndex() api
// const result = await services.callCluster('search', query);
const result = await services.callCluster('reindex', reIndex);
// TODO: Error handling here and writing of any errors that come back from ES by
services.log(['info', 'SIEM'], `Result of reindex: ${JSON.stringify(result, null, 2)}`);
} catch (err) {
// TODO: Error handling and writing of errors into a signal that has error
// handling/conditions
services.log(['error', 'SIEM'], `You encountered an error of: ${err.message}`);
}
// TODO: Error handling here and writing of any errors that come back from ES by
logger.info(`Result of reindex: ${JSON.stringify(result, null, 2)}`);
} catch (err) {
// TODO: Error handling and writing of errors into a signal that has error
// handling/conditions
logger.error(`You encountered an error of: ${err.message}`);
}
// Schedule the default action which is nothing if it's a plain signal.
instance.scheduleActions('default');
},
// Schedule the default action which is nothing if it's a plain signal.
instance.scheduleActions('default');
},
};
};

View file

@ -12,7 +12,7 @@ import mappings from './mappings.json';
import { migrations } from './migrations';
export { PluginSetupContract as TaskManager };
export { TaskInstance, ConcreteTaskInstance, TaskRunCreatorFunction } from './task';
export { TaskInstance, ConcreteTaskInstance, TaskRunCreatorFunction, RunContext } from './task';
export function taskManager(kibana: any) {
return new kibana.Plugin({

View file

@ -314,12 +314,12 @@ export default function(kibana: any) {
actionGroups: [],
async executor({ services, params, state }: AlertExecutorOptions) {},
};
server.plugins.alerting.registerType(alwaysFiringAlertType);
server.plugins.alerting.registerType(neverFiringAlertType);
server.plugins.alerting.registerType(failingAlertType);
server.plugins.alerting.registerType(validationAlertType);
server.plugins.alerting.registerType(authorizationAlertType);
server.plugins.alerting.registerType(noopAlertType);
server.plugins.alerting.setup.registerType(alwaysFiringAlertType);
server.plugins.alerting.setup.registerType(neverFiringAlertType);
server.plugins.alerting.setup.registerType(failingAlertType);
server.plugins.alerting.setup.registerType(validationAlertType);
server.plugins.alerting.setup.registerType(authorizationAlertType);
server.plugins.alerting.setup.registerType(noopAlertType);
},
});
}