[Uptime] migrate to observability rules registry (#100699)

* uptime - migrate to observability rules registry

* Modify Uptime alert types to work with server rule registry.

* Export `RuleType` type for consumption by client plugins.

* Add platinum as an option for `minimumLicenseRequired` field of `RuleTypeBase`.

* Simplify alert bootstrapping, inherit `RuleType` for alert factories.

* update rule field map

* adjust rule registery to be created within setup instead of mount

* adjust plugin setup to account for rule registry changes

* export types from rule registry

* move alert action message translations to common

* update rule field map

* update monitor status public alert model

* update tls public alert model

* update monitor status alert server model

* update tls alert sever model

* update server plugin file to scope alerts indices to synthetics

* add initContext to server Plugin class

* adjust public plugin to register alerts when core start is availabile

* update mappings

* update asset names

* adjust dependencies for alert initialization

* adjust duration anomaly server alert model

* adjust duration anomaly and monitor status public alert model to account for undefined types

* add duration_anomaly tests

* add anomaly severity

* adjust types

* update uptime server plugin

* remove test_helpers

* add getMonitorRouteFromMonitorId helper

* export AlertTypeWithExecutor from rule_registry

* adjust types

* mock time zone

* update types

* update types for legacy tls alert

* update mappings

* update monitor status check tls types

* update tls types and indexed fields

* update duration anomaly types and indexed fields

* update mappings

* delete unnecessary file

* adjust types

* adjust ruleDataClient initialization

* index anomaly bucket span

* update types

* adjust registration of legacy tls alert type

* adjust types

* update index alias name

* update anomaly detection rule mappings

* adjust import for certificate alert

* adjust rbac settings

* adjust content

* adjust uptime server plugin

Co-authored-by: Justin Kambic <justin.kambic@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dominique Clarke 2021-07-16 15:34:15 -04:00 committed by GitHub
parent ce5a798fdd
commit 743c8c2a14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1813 additions and 1103 deletions

View file

@ -14,13 +14,17 @@ export type { RacRequestHandlerContext, RacApiRequestHandlerContext } from './ty
export { RuleDataClient } from './rule_data_client';
export { IRuleDataClient } from './rule_data_client/types';
export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data';
export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory';
export {
createLifecycleRuleTypeFactory,
LifecycleAlertService,
} from './utils/create_lifecycle_rule_type_factory';
export {
LifecycleRuleExecutor,
LifecycleAlertServices,
createLifecycleExecutor,
} from './utils/create_lifecycle_executor';
export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory';
export type { AlertTypeWithExecutor } from './types';
export const plugin = (initContext: PluginInitializerContext) =>
new RuleRegistryPlugin(initContext);

View file

@ -16,10 +16,13 @@ import { AlertInstance } from '../../../alerting/server';
import { AlertTypeWithExecutor } from '../types';
import { createLifecycleExecutor } from './create_lifecycle_executor';
export type LifecycleAlertService<TAlertInstanceContext extends Record<string, unknown>> = (alert: {
export type LifecycleAlertService<
TAlertInstanceContext extends Record<string, unknown>,
TActionGroupIds extends string = string
> = (alert: {
id: string;
fields: Record<string, unknown>;
}) => AlertInstance<AlertInstanceState, TAlertInstanceContext, string>;
}) => AlertInstance<AlertInstanceState, TAlertInstanceContext, TActionGroupIds>;
export const createLifecycleRuleTypeFactory = ({
logger,

View file

@ -19,6 +19,7 @@ export const mapConsumerToIndexName = {
infrastructure: '.alerts-observability.metrics',
observability: '.alerts-observability',
siem: ['.alerts-security.alerts', '.siem-signals'],
synthetics: '.alerts-observability-synthetics',
};
export type ValidFeatureId = keyof typeof mapConsumerToIndexName;

View file

@ -0,0 +1,60 @@
/*
* 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 uptimeRuleFieldMap = {
// common fields
'monitor.id': {
type: 'keyword',
},
'url.full': {
type: 'keyword',
},
'observer.geo.name': {
type: 'keyword',
},
reason: {
type: 'text',
},
// monitor status alert fields
'error.message': {
type: 'text',
},
'agent.name': {
type: 'keyword',
},
'monitor.name': {
type: 'keyword',
},
'monitor.type': {
type: 'keyword',
},
// tls alert fields
'tls.server.x509.issuer.common_name': {
type: 'keyword',
},
'tls.server.x509.subject.common_name': {
type: 'keyword',
},
'tls.server.x509.not_after': {
type: 'date',
},
'tls.server.x509.not_before': {
type: 'date',
},
'tls.server.hash.sha256': {
type: 'keyword',
},
// anomaly alert fields
'anomaly.start': {
type: 'date',
},
'anomaly.bucket_span.minutes': {
type: 'keyword',
},
} as const;
export type UptimeRuleFieldMap = typeof uptimeRuleFieldMap;

View file

@ -37,3 +37,78 @@ export const MonitorStatusTranslations = {
defaultMessage: 'Alert when a monitor is down or an availability threshold is breached.',
}),
};
export const TlsTranslations = {
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', {
defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary}
`,
values: {
commonName: '{{state.commonName}}',
issuer: '{{state.issuer}}',
summary: '{{state.summary}}',
status: '{{state.status}}',
},
}),
name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', {
defaultMessage: 'Uptime TLS (Legacy)',
}),
description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', {
defaultMessage:
'Alert when the TLS certificate of an Uptime monitor is about to expire. This alert will be deprecated in a future version.',
}),
};
export const TlsTranslationsLegacy = {
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', {
defaultMessage: `Detected {count} TLS certificates expiring or becoming too old.
{expiringConditionalOpen}
Expiring cert count: {expiringCount}
Expiring Certificates: {expiringCommonNameAndDate}
{expiringConditionalClose}
{agingConditionalOpen}
Aging cert count: {agingCount}
Aging Certificates: {agingCommonNameAndDate}
{agingConditionalClose}
`,
values: {
count: '{{state.count}}',
expiringCount: '{{state.expiringCount}}',
expiringCommonNameAndDate: '{{state.expiringCommonNameAndDate}}',
expiringConditionalOpen: '{{#state.hasExpired}}',
expiringConditionalClose: '{{/state.hasExpired}}',
agingCount: '{{state.agingCount}}',
agingCommonNameAndDate: '{{state.agingCommonNameAndDate}}',
agingConditionalOpen: '{{#state.hasAging}}',
agingConditionalClose: '{{/state.hasAging}}',
},
}),
name: i18n.translate('xpack.uptime.alerts.tls.clientName', {
defaultMessage: 'Uptime TLS',
}),
description: i18n.translate('xpack.uptime.alerts.tls.description', {
defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.',
}),
};
export const DurationAnomalyTranslations = {
defaultActionMessage: i18n.translate('xpack.uptime.alerts.durationAnomaly.defaultActionMessage', {
defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}.
Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`,
values: {
severity: '{{state.severity}}',
anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}',
monitor: '{{state.monitor}}',
monitorUrl: '{{{state.monitorUrl}}}',
slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}',
expectedResponseTime: '{{state.expectedResponseTime}}',
severityScore: '{{state.severityScore}}',
observerLocation: '{{state.observerLocation}}',
},
}),
name: i18n.translate('xpack.uptime.alerts.durationAnomaly.clientName', {
defaultMessage: 'Uptime Duration Anomaly',
}),
description: i18n.translate('xpack.uptime.alerts.durationAnomaly.description', {
defaultMessage: 'Alert when the Uptime monitor duration is anaomalous.',
}),
};

View file

@ -1,8 +1,16 @@
{
"configPath": ["xpack", "uptime"],
"configPath": [
"xpack",
"uptime"
],
"id": "uptime",
"kibanaVersion": "kibana",
"optionalPlugins": ["data", "home", "ml", "fleet"],
"optionalPlugins": [
"data",
"home",
"ml",
"fleet"
],
"requiredPlugins": [
"alerting",
"embeddable",
@ -10,15 +18,24 @@
"licensing",
"triggersActionsUi",
"usageCollection",
"ruleRegistry",
"observability"
],
"server": true,
"ui": true,
"version": "8.0.0",
"requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml", "fleet"],
"requiredBundles": [
"observability",
"kibanaReact",
"kibanaUtils",
"home",
"data",
"ml",
"fleet"
],
"owner": {
"name": "Uptime",
"githubTeam": "uptime"
},
"description": "This plugin visualizes data from Synthetics and Heartbeat, and integrates with other Observability solutions."
}
}

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
CoreSetup,
CoreStart,
@ -29,7 +28,7 @@ import {
DataPublicPluginSetup,
DataPublicPluginStart,
} from '../../../../../src/plugins/data/public';
import { alertTypeInitializers } from '../lib/alert_types';
import { alertTypeInitializers, legacyAlertTypeInitializers } from '../lib/alert_types';
import { FleetStart } from '../../../fleet/public';
import {
FetchDataParams,
@ -141,6 +140,36 @@ export class UptimePlugin
)
);
const { observabilityRuleTypeRegistry } = plugins.observability;
core.getStartServices().then(([coreStart, clientPluginsStart]) => {
alertTypeInitializers.forEach((init) => {
const alertInitializer = init({
core: coreStart,
plugins: clientPluginsStart,
});
if (
clientPluginsStart.triggersActionsUi &&
!clientPluginsStart.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id)
) {
observabilityRuleTypeRegistry.register(alertInitializer);
}
});
legacyAlertTypeInitializers.forEach((init) => {
const alertInitializer = init({
core: coreStart,
plugins: clientPluginsStart,
});
if (
clientPluginsStart.triggersActionsUi &&
!clientPluginsStart.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id)
) {
plugins.triggersActionsUi.alertTypeRegistry.register(alertInitializer);
}
});
});
core.application.register({
id: PLUGIN.ID,
euiIconType: 'logoObservability',
@ -171,26 +200,12 @@ export class UptimePlugin
const [coreStart, corePlugins] = await core.getStartServices();
const { renderApp } = await import('./render_app');
return renderApp(coreStart, plugins, corePlugins, params);
},
});
}
public start(start: CoreStart, plugins: ClientPluginsStart): void {
alertTypeInitializers.forEach((init) => {
const alertInitializer = init({
core: start,
plugins,
});
if (
plugins.triggersActionsUi &&
!plugins.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id)
) {
plugins.triggersActionsUi.alertTypeRegistry.register(alertInitializer);
}
});
if (plugins.fleet) {
const { registerExtension } = plugins.fleet;

View file

@ -0,0 +1,40 @@
/*
* 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 { stringify } from 'querystring';
export const format = ({
pathname,
query,
}: {
pathname: string;
query: Record<string, any>;
}): string => {
return `${pathname}?${stringify(query)}`;
};
export const getMonitorRouteFromMonitorId = ({
monitorId,
dateRangeStart,
dateRangeEnd,
filters = {},
}: {
monitorId: string;
dateRangeStart: string;
dateRangeEnd: string;
filters?: Record<string, string[]>;
}) =>
format({
pathname: `/app/uptime/monitor/${btoa(monitorId)}`,
query: {
dateRangeEnd,
dateRangeStart,
...(Object.keys(filters).length
? { filters: JSON.stringify(Object.keys(filters).map((key) => [key, filters[key]])) }
: {}),
},
});

View file

@ -6,18 +6,23 @@
*/
import React from 'react';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import moment from 'moment';
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
import { DurationAnomalyTranslations } from './translations';
import { DurationAnomalyTranslations } from '../../../common/translations';
import { AlertTypeInitializer } from '.';
import { getMonitorRouteFromMonitorId } from './common';
import { ObservabilityRuleTypeModel } from '../../../../observability/public';
const { defaultActionMessage, description } = DurationAnomalyTranslations;
const DurationAnomalyAlert = React.lazy(() => import('./lazy_wrapper/duration_anomaly'));
export const initDurationAnomalyAlertType: AlertTypeInitializer = ({
core,
plugins,
}): AlertTypeModel => ({
}): ObservabilityRuleTypeModel => ({
id: CLIENT_ALERT_TYPES.DURATION_ANOMALY,
iconClass: 'uptimeApp',
documentationUrl(docLinks) {
@ -30,4 +35,13 @@ export const initDurationAnomalyAlertType: AlertTypeInitializer = ({
validate: () => ({ errors: {} }),
defaultActionMessage,
requiresAppContext: true,
format: ({ fields }) => ({
reason: fields.reason,
link: getMonitorRouteFromMonitorId({
monitorId: fields['monitor.id']!,
dateRangeEnd:
fields['kibana.rac.alert.status'] === 'open' ? 'now' : fields['kibana.rac.alert.end']!,
dateRangeStart: moment(new Date(fields['anomaly.start']!)).subtract('5', 'm').toISOString(),
}),
}),
});

View file

@ -6,6 +6,7 @@
*/
import { CoreStart } from 'kibana/public';
import { ObservabilityRuleTypeModel } from '../../../../observability/public';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { initMonitorStatusAlertType } from './monitor_status';
import { initTlsAlertType } from './tls';
@ -13,14 +14,17 @@ import { initTlsLegacyAlertType } from './tls_legacy';
import { ClientPluginsStart } from '../../apps/plugin';
import { initDurationAnomalyAlertType } from './duration_anomaly';
export type AlertTypeInitializer = (dependenies: {
export type AlertTypeInitializer<TAlertTypeModel = ObservabilityRuleTypeModel> = (dependenies: {
core: CoreStart;
plugins: ClientPluginsStart;
}) => AlertTypeModel;
}) => TAlertTypeModel;
export const alertTypeInitializers: AlertTypeInitializer[] = [
initMonitorStatusAlertType,
initTlsAlertType,
initTlsLegacyAlertType,
initDurationAnomalyAlertType,
];
export const legacyAlertTypeInitializers: Array<AlertTypeInitializer<AlertTypeModel>> = [
initTlsLegacyAlertType,
];

View file

@ -206,6 +206,7 @@ describe('monitor status alert type', () => {
"defaultActionMessage": "Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}}",
"description": "Alert when a monitor is down or an availability threshold is breached.",
"documentationUrl": [Function],
"format": [Function],
"iconClass": "uptimeApp",
"id": "xpack.uptime.alerts.monitorStatus",
"requiresAppContext": false,

View file

@ -6,12 +6,18 @@
*/
import React from 'react';
import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public';
import { AlertTypeInitializer } from '.';
import moment from 'moment';
import { ObservabilityRuleTypeModel } from '../../../../observability/public';
import { ValidationResult } from '../../../../triggers_actions_ui/public';
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
import { MonitorStatusTranslations } from '../../../common/translations';
import { getMonitorRouteFromMonitorId } from './common';
import { AlertTypeInitializer } from '.';
const { defaultActionMessage, description } = MonitorStatusTranslations;
const MonitorStatusAlert = React.lazy(() => import('./lazy_wrapper/monitor_status'));
@ -21,7 +27,7 @@ let validateFunc: (alertParams: any) => ValidationResult;
export const initMonitorStatusAlertType: AlertTypeInitializer = ({
core,
plugins,
}): AlertTypeModel => ({
}): ObservabilityRuleTypeModel => ({
id: CLIENT_ALERT_TYPES.MONITOR_STATUS,
description,
iconClass: 'uptimeApp',
@ -44,4 +50,18 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({
},
defaultActionMessage,
requiresAppContext: false,
format: ({ fields }) => ({
reason: fields.reason,
link: getMonitorRouteFromMonitorId({
monitorId: fields['monitor.id']!,
dateRangeEnd:
fields['kibana.rac.alert.status'] === 'open' ? 'now' : fields['kibana.rac.alert.end']!,
dateRangeStart: moment(new Date(fields['kibana.rac.alert.start']!))
.subtract('5', 'm')
.toISOString(),
filters: {
'observer.geo.name': [fields['observer.geo.name'][0]],
},
}),
}),
});

View file

@ -6,14 +6,19 @@
*/
import React from 'react';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { ObservabilityRuleTypeModel } from '../../../../observability/public';
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
import { TlsTranslations } from './translations';
import { TlsTranslations } from '../../../common/translations';
import { AlertTypeInitializer } from '.';
import { CERTIFICATES_ROUTE } from '../../../common/constants/ui';
const { defaultActionMessage, description } = TlsTranslations;
const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert'));
export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): AlertTypeModel => ({
export const initTlsAlertType: AlertTypeInitializer = ({
core,
plugins,
}): ObservabilityRuleTypeModel => ({
id: CLIENT_ALERT_TYPES.TLS,
iconClass: 'uptimeApp',
documentationUrl(docLinks) {
@ -26,4 +31,8 @@ export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): Alert
validate: () => ({ errors: {} }),
defaultActionMessage,
requiresAppContext: false,
format: ({ fields }) => ({
reason: fields.reason,
link: `/app/uptime${CERTIFICATES_ROUTE}`,
}),
});

View file

@ -8,12 +8,12 @@
import React from 'react';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
import { TlsTranslationsLegacy } from './translations';
import { TlsTranslationsLegacy } from '../../../common/translations';
import { AlertTypeInitializer } from '.';
const { defaultActionMessage, description } = TlsTranslationsLegacy;
const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert'));
export const initTlsLegacyAlertType: AlertTypeInitializer = ({
export const initTlsLegacyAlertType: AlertTypeInitializer<AlertTypeModel> = ({
core,
plugins,
}): AlertTypeModel => ({

View file

@ -1,83 +0,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 { i18n } from '@kbn/i18n';
export const TlsTranslations = {
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', {
defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary}
`,
values: {
commonName: '{{state.commonName}}',
issuer: '{{state.issuer}}',
summary: '{{state.summary}}',
status: '{{state.status}}',
},
}),
name: i18n.translate('xpack.uptime.alerts.tls.clientName', {
defaultMessage: 'Uptime TLS',
}),
description: i18n.translate('xpack.uptime.alerts.tls.description', {
defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.',
}),
};
export const TlsTranslationsLegacy = {
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', {
defaultMessage: `Detected {count} TLS certificates expiring or becoming too old.
{expiringConditionalOpen}
Expiring cert count: {expiringCount}
Expiring Certificates: {expiringCommonNameAndDate}
{expiringConditionalClose}
{agingConditionalOpen}
Aging cert count: {agingCount}
Aging Certificates: {agingCommonNameAndDate}
{agingConditionalClose}
`,
values: {
count: '{{state.count}}',
expiringCount: '{{state.expiringCount}}',
expiringCommonNameAndDate: '{{state.expiringCommonNameAndDate}}',
expiringConditionalOpen: '{{#state.hasExpired}}',
expiringConditionalClose: '{{/state.hasExpired}}',
agingCount: '{{state.agingCount}}',
agingCommonNameAndDate: '{{state.agingCommonNameAndDate}}',
agingConditionalOpen: '{{#state.hasAging}}',
agingConditionalClose: '{{/state.hasAging}}',
},
}),
name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', {
defaultMessage: 'Uptime TLS',
}),
description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', {
defaultMessage:
'Alert when the TLS certificate of an Uptime monitor is about to expire. This rule type will be deprecated in a future version.',
}),
};
export const DurationAnomalyTranslations = {
defaultActionMessage: i18n.translate('xpack.uptime.alerts.durationAnomaly.defaultActionMessage', {
defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}.
Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`,
values: {
severity: '{{state.severity}}',
anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}',
monitor: '{{state.monitor}}',
monitorUrl: '{{{state.monitorUrl}}}',
slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}',
expectedResponseTime: '{{state.expectedResponseTime}}',
severityScore: '{{state.severityScore}}',
observerLocation: '{{state.observerLocation}}',
},
}),
name: i18n.translate('xpack.uptime.alerts.durationAnomaly.clientName', {
defaultMessage: 'Uptime Duration Anomaly',
}),
description: i18n.translate('xpack.uptime.alerts.durationAnomaly.description', {
defaultMessage: 'Alert when the Uptime monitor duration is anaomalous.',
}),
};

View file

@ -6,12 +6,14 @@
*/
import { Request, Server } from '@hapi/hapi';
import { Logger } from 'kibana/server';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PLUGIN } from '../common/constants/plugin';
import { compose } from './lib/compose/kibana';
import { initUptimeServer } from './uptime_server';
import { UptimeCorePlugins, UptimeCoreSetup } from './lib/adapters/framework';
import { umDynamicSettings } from './lib/saved_objects';
import { UptimeRuleRegistry } from './plugin';
export interface KibanaRouteOptions {
path: string;
@ -25,7 +27,12 @@ export interface KibanaServer extends Server {
route: (options: KibanaRouteOptions) => void;
}
export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCorePlugins) => {
export const initServerWithKibana = (
server: UptimeCoreSetup,
plugins: UptimeCorePlugins,
ruleRegistry: UptimeRuleRegistry,
logger: Logger
) => {
const { features } = plugins;
const libs = compose(server);
@ -86,5 +93,5 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
},
});
initUptimeServer(server, libs, plugins);
initUptimeServer(server, libs, plugins, ruleRegistry, logger);
};

View file

@ -11,9 +11,11 @@ import type {
ISavedObjectsRepository,
IScopedClusterClient,
} from 'src/core/server';
import { ObservabilityPluginSetup } from '../../../../../observability/server';
import { UMKibanaRoute } from '../../../rest_api';
import { PluginSetupContract } from '../../../../../features/server';
import { MlPluginSetup as MlSetup } from '../../../../../ml/server';
import { RuleRegistryPluginSetupContract } from '../../../../../rule_registry/server';
import { UptimeESClient } from '../../lib';
import type { UptimeRouter } from '../../../types';
@ -37,8 +39,10 @@ export interface UptimeCorePlugins {
features: PluginSetupContract;
alerting: any;
elasticsearch: any;
observability: ObservabilityPluginSetup;
usageCollection: UsageCollectionSetup;
ml: MlSetup;
ruleRegistry: RuleRegistryPluginSetupContract;
}
export interface UMBackendFrameworkAdapter {

View file

@ -6,6 +6,7 @@
*/
import { isRight } from 'fp-ts/lib/Either';
import Mustache from 'mustache';
import { UptimeCommonState, UptimeCommonStateType } from '../../../common/runtime_types';
export type UpdateUptimeAlertState = (
@ -55,3 +56,7 @@ export const updateState: UpdateUptimeAlertState = (state, isTriggeredNow) => {
isTriggered: isTriggeredNow,
};
};
export const generateAlertMessage = (messageTemplate: string, fields: Record<string, any>) => {
return Mustache.render(messageTemplate, { state: { ...fields } });
};

View file

@ -0,0 +1,210 @@
/*
* 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 { durationAnomalyAlertFactory } from './duration_anomaly';
import { DURATION_ANOMALY } from '../../../common/constants/alerts';
import { AnomaliesTableRecord, AnomalyRecordDoc } from '../../../../ml/common/types/anomalies';
import { DynamicSettings } from '../../../common/runtime_types';
import { createRuleTypeMocks, bootstrapDependencies } from './test_utils';
import { getSeverityType } from '../../../../ml/common/util/anomaly_utils';
import { Ping } from '../../../common/runtime_types/ping';
import {
ALERT_SEVERITY_LEVEL,
ALERT_SEVERITY_VALUE,
ALERT_EVALUATION_VALUE,
ALERT_EVALUATION_THRESHOLD,
} from '@kbn/rule-data-utils/target/technical_field_names';
interface MockAnomaly {
severity: AnomaliesTableRecord['severity'];
source: Partial<AnomalyRecordDoc>;
actualSort: AnomaliesTableRecord['actualSort'];
typicalSort: AnomaliesTableRecord['typicalSort'];
entityValue: AnomaliesTableRecord['entityValue'];
}
interface MockAnomalyResult {
anomalies: MockAnomaly[];
}
const monitorId = 'uptime-monitor';
const mockUrl = 'https://elastic.co';
/**
* This function aims to provide an easy way to give mock props that will
* reduce boilerplate for tests.
* @param dynamic the expiration and aging thresholds received at alert creation time
* @param params the params received at alert creation time
* @param state the state the alert maintains
*/
const mockOptions = (
dynamicCertSettings?: {
certExpirationThreshold: DynamicSettings['certExpirationThreshold'];
certAgeThreshold: DynamicSettings['certAgeThreshold'];
},
state = {},
params = {
timerange: { from: 'now-15m', to: 'now' },
monitorId,
severity: 'warning',
}
): any => {
const { services } = createRuleTypeMocks(dynamicCertSettings);
return {
params,
state,
services,
};
};
const mockAnomaliesResult: MockAnomalyResult = {
anomalies: [
{
severity: 25,
source: {
timestamp: 1622137799,
'monitor.id': 'uptime-monitor',
bucket_span: 900,
},
actualSort: 200000,
typicalSort: 10000,
entityValue: 'harrisburg',
},
{
severity: 10,
source: {
timestamp: 1622137799,
'monitor.id': 'uptime-monitor',
bucket_span: 900,
},
actualSort: 300000,
typicalSort: 20000,
entityValue: 'fairbanks',
},
],
};
const mockPing: Partial<Ping> = {
url: {
full: mockUrl,
},
};
describe('duration anomaly alert', () => {
let toISOStringSpy: jest.SpyInstance<string, []>;
const mockDate = 'date';
beforeAll(() => {
Date.now = jest.fn().mockReturnValue(new Date('2021-05-13T12:33:37.000Z'));
jest.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => ({
format: jest.fn(),
formatToParts: jest.fn(),
resolvedOptions: () => ({
locale: '',
calendar: '',
numberingSystem: '',
timeZone: 'UTC',
}),
}));
toISOStringSpy = jest.spyOn(Date.prototype, 'toISOString');
});
describe('alert executor', () => {
it('triggers when aging or expiring alerts are found', async () => {
toISOStringSpy.mockImplementation(() => mockDate);
const mockResultServiceProviderGetter: jest.Mock<{
getAnomaliesTableData: jest.Mock<MockAnomalyResult>;
}> = jest.fn();
const mockGetAnomliesTableDataGetter: jest.Mock<MockAnomalyResult> = jest.fn();
const mockGetLatestMonitorGetter: jest.Mock<Partial<Ping>> = jest.fn();
mockGetLatestMonitorGetter.mockReturnValue(mockPing);
mockGetAnomliesTableDataGetter.mockReturnValue(mockAnomaliesResult);
mockResultServiceProviderGetter.mockReturnValue({
getAnomaliesTableData: mockGetAnomliesTableDataGetter,
});
const { server, libs, plugins } = bootstrapDependencies(
{ getLatestMonitor: mockGetLatestMonitorGetter },
{
ml: {
resultsServiceProvider: mockResultServiceProviderGetter,
},
}
);
const alert = durationAnomalyAlertFactory(server, libs, plugins);
const options = mockOptions();
const {
services: { alertWithLifecycle },
} = options;
// @ts-ignore the executor can return `void`, but ours never does
const state: Record<string, any> = await alert.executor(options);
expect(mockGetAnomliesTableDataGetter).toHaveBeenCalledTimes(1);
expect(alertWithLifecycle).toHaveBeenCalledTimes(2);
expect(mockGetAnomliesTableDataGetter).toBeCalledWith(
['uptime_monitor_high_latency_by_geo'],
[],
[],
'auto',
options.params.severity,
1620909217000,
1620909217000,
'UTC',
500,
10,
undefined
);
const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results;
expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2);
mockAnomaliesResult.anomalies.forEach((anomaly, index) => {
const slowestResponse = Math.round(anomaly.actualSort / 1000);
const typicalResponse = Math.round(anomaly.typicalSort / 1000);
expect(alertWithLifecycle).toBeCalledWith({
fields: {
'monitor.id': options.params.monitorId,
'url.full': mockPing.url?.full,
'anomaly.start': mockDate,
'anomaly.bucket_span.minutes': anomaly.source.bucket_span,
'observer.geo.name': anomaly.entityValue,
[ALERT_EVALUATION_VALUE]: anomaly.actualSort,
[ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort,
[ALERT_SEVERITY_LEVEL]: getSeverityType(anomaly.severity),
[ALERT_SEVERITY_VALUE]: anomaly.severity,
reason: `Abnormal (${getSeverityType(
anomaly.severity
)} level) response time detected on uptime-monitor with url ${
mockPing.url?.full
} at date. Anomaly severity score is ${anomaly.severity}.
Response times as high as ${slowestResponse} ms have been detected from location ${
anomaly.entityValue
}. Expected response time is ${typicalResponse} ms.`,
},
id: `${DURATION_ANOMALY.id}${index}`,
});
expect(alertInstanceMock.replaceState).toBeCalledWith({
firstCheckedAt: 'date',
firstTriggeredAt: undefined,
lastCheckedAt: 'date',
lastResolvedAt: undefined,
isTriggered: false,
anomalyStartTimestamp: 'date',
currentTriggerStarted: undefined,
expectedResponseTime: `${typicalResponse} ms`,
lastTriggeredAt: undefined,
monitor: monitorId,
monitorUrl: mockPing.url?.full,
observerLocation: anomaly.entityValue,
severity: getSeverityType(anomaly.severity),
severityScore: anomaly.severity,
slowestAnomalyResponse: `${slowestResponse} ms`,
bucketSpan: anomaly.source.bucket_span,
});
});
expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(2);
expect(alertInstanceMock.scheduleActions).toBeCalledWith(DURATION_ANOMALY.id);
});
});
});

View file

@ -8,8 +8,14 @@
import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
import moment from 'moment';
import { schema } from '@kbn/config-schema';
import {
ALERT_SEVERITY_LEVEL,
ALERT_SEVERITY_VALUE,
ALERT_EVALUATION_VALUE,
ALERT_EVALUATION_THRESHOLD,
} from '@kbn/rule-data-utils/target/technical_field_names';
import { ActionGroupIdsOf } from '../../../../alerting/common';
import { updateState } from './common';
import { updateState, generateAlertMessage } from './common';
import { DURATION_ANOMALY } from '../../../common/constants/alerts';
import { commonStateTranslations, durationAnomalyTranslations } from './translations';
import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies';
@ -18,8 +24,10 @@ import { UptimeCorePlugins } from '../adapters/framework';
import { UptimeAlertTypeFactory } from './types';
import { Ping } from '../../../common/runtime_types/ping';
import { getMLJobId } from '../../../common/lib';
import { getLatestMonitor } from '../requests/get_latest_monitor';
import { uptimeAlertWrapper } from './uptime_alert_wrapper';
import { DurationAnomalyTranslations as CommonDurationAnomalyTranslations } from '../../../common/translations';
import { createUptimeESClient } from '../lib';
export type ActionGroupIds = ActionGroupIdsOf<typeof DURATION_ANOMALY>;
@ -33,6 +41,7 @@ export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Pi
slowestAnomalyResponse: Math.round(anomaly.actualSort / 1000) + ' ms',
expectedResponseTime: Math.round(anomaly.typicalSort / 1000) + ' ms',
observerLocation: anomaly.entityValue,
bucketSpan: anomaly.source.bucket_span,
};
};
@ -65,61 +74,83 @@ const getAnomalies = async (
export const durationAnomalyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (
_server,
_libs,
libs,
plugins
) =>
uptimeAlertWrapper<ActionGroupIds>({
id: 'xpack.uptime.alerts.durationAnomaly',
name: durationAnomalyTranslations.alertFactoryName,
validate: {
params: schema.object({
monitorId: schema.string(),
severity: schema.number(),
}),
) => ({
id: 'xpack.uptime.alerts.durationAnomaly',
producer: 'uptime',
name: durationAnomalyTranslations.alertFactoryName,
validate: {
params: schema.object({
monitorId: schema.string(),
severity: schema.number(),
}),
},
defaultActionGroupId: DURATION_ANOMALY.id,
actionGroups: [
{
id: DURATION_ANOMALY.id,
name: DURATION_ANOMALY.name,
},
defaultActionGroupId: DURATION_ANOMALY.id,
actionGroups: [
{
id: DURATION_ANOMALY.id,
name: DURATION_ANOMALY.name,
},
],
actionVariables: {
context: [],
state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations],
},
minimumLicenseRequired: 'basic',
isExportable: true,
async executor({ options, uptimeEsClient, savedObjectsClient, dynamicSettings }) {
const {
services: { alertInstanceFactory },
state,
params,
} = options;
],
actionVariables: {
context: [],
state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations],
},
isExportable: true,
minimumLicenseRequired: 'platinum',
async executor({
params,
services: { alertWithLifecycle, scopedClusterClient, savedObjectsClient },
state,
}) {
const uptimeEsClient = createUptimeESClient({
esClient: scopedClusterClient.asCurrentUser,
savedObjectsClient,
});
const { anomalies } =
(await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt as string)) ??
{};
const { anomalies } =
(await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt)) ?? {};
const foundAnomalies = anomalies?.length > 0;
const foundAnomalies = anomalies?.length > 0;
if (foundAnomalies) {
const monitorInfo = await libs.requests.getLatestMonitor({
uptimeEsClient,
dateStart: 'now-15m',
dateEnd: 'now',
monitorId: params.monitorId,
});
if (foundAnomalies) {
const monitorInfo = await getLatestMonitor({
uptimeEsClient,
dateStart: 'now-15m',
dateEnd: 'now',
monitorId: params.monitorId,
anomalies.forEach((anomaly, index) => {
const summary = getAnomalySummary(anomaly, monitorInfo);
const alertInstance = alertWithLifecycle({
id: DURATION_ANOMALY.id + index,
fields: {
'monitor.id': params.monitorId,
'url.full': summary.monitorUrl,
'observer.geo.name': summary.observerLocation,
'anomaly.start': summary.anomalyStartTimestamp,
'anomaly.bucket_span.minutes': summary.bucketSpan,
[ALERT_EVALUATION_VALUE]: anomaly.actualSort,
[ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort,
[ALERT_SEVERITY_LEVEL]: summary.severity,
[ALERT_SEVERITY_VALUE]: summary.severityScore,
reason: generateAlertMessage(
CommonDurationAnomalyTranslations.defaultActionMessage,
summary
),
},
});
anomalies.forEach((anomaly, index) => {
const alertInstance = alertInstanceFactory(DURATION_ANOMALY.id + index);
const summary = getAnomalySummary(anomaly, monitorInfo);
alertInstance.replaceState({
...updateState(state, false),
...summary,
});
alertInstance.scheduleActions(DURATION_ANOMALY.id);
alertInstance.replaceState({
...updateState(state, false),
...summary,
});
}
alertInstance.scheduleActions(DURATION_ANOMALY.id);
});
}
return updateState(state, foundAnomalies);
},
});
return updateState(state, foundAnomalies);
},
});

File diff suppressed because it is too large Load diff

View file

@ -4,13 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import datemath from '@elastic/datemath';
import { min } from 'lodash';
import datemath from '@elastic/datemath';
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import Mustache from 'mustache';
import { JsonObject } from '@kbn/common-utils';
import { ActionGroupIdsOf } from '../../../../alerting/common';
import { UptimeAlertTypeFactory } from './types';
import { esKuery } from '../../../../../../src/plugins/data/server';
import {
@ -19,16 +17,16 @@ import {
GetMonitorAvailabilityParams,
} from '../../../common/runtime_types';
import { MONITOR_STATUS } from '../../../common/constants/alerts';
import { updateState } from './common';
import { updateState, generateAlertMessage } from './common';
import { commonMonitorStateI18, commonStateTranslations, DOWN_LABEL } from './translations';
import { stringifyKueries, combineFiltersAndUserSearch } from '../../../common/lib';
import { GetMonitorAvailabilityResult } from '../requests/get_monitor_availability';
import { GetMonitorStatusResult } from '../requests/get_monitor_status';
import { UNNAMED_LOCATION } from '../../../common/constants';
import { uptimeAlertWrapper } from './uptime_alert_wrapper';
import { MonitorStatusTranslations } from '../../../common/translations';
import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/get_index_pattern';
import { UMServerLibs, UptimeESClient } from '../lib';
import { UMServerLibs, UptimeESClient, createUptimeESClient } from '../lib';
import { ActionGroupIdsOf } from '../../../../alerting/common';
export type ActionGroupIds = ActionGroupIdsOf<typeof MONITOR_STATUS>;
@ -134,8 +132,8 @@ export const formatFilterString = async (
search
);
export const getMonitorSummary = (monitorInfo: Ping) => {
return {
export const getMonitorSummary = (monitorInfo: Ping, statusMessage: string) => {
const summary = {
monitorUrl: monitorInfo.url?.full,
monitorId: monitorInfo.monitor?.id,
monitorName: monitorInfo.monitor?.name ?? monitorInfo.monitor?.id,
@ -144,16 +142,26 @@ export const getMonitorSummary = (monitorInfo: Ping) => {
observerLocation: monitorInfo.observer?.geo?.name ?? UNNAMED_LOCATION,
observerHostname: monitorInfo.agent?.name,
};
const reason = generateAlertMessage(MonitorStatusTranslations.defaultActionMessage, {
...summary,
statusMessage,
});
return {
...summary,
reason,
};
};
const generateMessageForOlderVersions = (fields: Record<string, any>) => {
const messageTemplate = MonitorStatusTranslations.defaultActionMessage;
// Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from
// {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}}
return Mustache.render(messageTemplate, { state: { ...fields } });
};
export const getMonitorAlertDocument = (monitorSummary: Record<string, string | undefined>) => ({
'monitor.id': monitorSummary.monitorId,
'monitor.type': monitorSummary.monitorType,
'monitor.name': monitorSummary.monitorName,
'url.full': monitorSummary.monitorUrl,
'observer.geo.name': monitorSummary.observerLocation,
'error.message': monitorSummary.latestErrorMessage,
'agent.name': monitorSummary.observerHostname,
reason: monitorSummary.reason,
});
export const getStatusMessage = (
downMonInfo?: Ping,
@ -194,7 +202,7 @@ export const getStatusMessage = (
return statusMessage + availabilityMessage;
};
const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => {
export const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => {
const normalizeText = (txt: string) => {
// replace url and name special characters with -
return txt.replace(/[^A-Z0-9]+/gi, '_').toLowerCase();
@ -209,200 +217,204 @@ const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => {
return `${urlText}_${monIdByLoc}`;
};
export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) =>
uptimeAlertWrapper<ActionGroupIds>({
id: 'xpack.uptime.alerts.monitorStatus',
name: i18n.translate('xpack.uptime.alerts.monitorStatus', {
defaultMessage: 'Uptime monitor status',
export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) => ({
id: 'xpack.uptime.alerts.monitorStatus',
producer: 'uptime',
name: i18n.translate('xpack.uptime.alerts.monitorStatus', {
defaultMessage: 'Uptime monitor status',
}),
validate: {
params: schema.object({
availability: schema.maybe(
schema.object({
range: schema.number(),
rangeUnit: schema.string(),
threshold: schema.string(),
})
),
filters: schema.maybe(
schema.oneOf([
// deprecated
schema.object({
'monitor.type': schema.maybe(schema.arrayOf(schema.string())),
'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())),
tags: schema.maybe(schema.arrayOf(schema.string())),
'url.port': schema.maybe(schema.arrayOf(schema.string())),
}),
schema.string(),
])
),
// deprecated
locations: schema.maybe(schema.arrayOf(schema.string())),
numTimes: schema.number(),
search: schema.maybe(schema.string()),
shouldCheckStatus: schema.boolean(),
shouldCheckAvailability: schema.boolean(),
timerangeCount: schema.maybe(schema.number()),
timerangeUnit: schema.maybe(schema.string()),
// deprecated
timerange: schema.maybe(
schema.object({
from: schema.string(),
to: schema.string(),
})
),
version: schema.maybe(schema.number()),
isAutoGenerated: schema.maybe(schema.boolean()),
}),
validate: {
params: schema.object({
availability: schema.maybe(
schema.object({
range: schema.number(),
rangeUnit: schema.string(),
threshold: schema.string(),
})
),
filters: schema.maybe(
schema.oneOf([
// deprecated
schema.object({
'monitor.type': schema.maybe(schema.arrayOf(schema.string())),
'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())),
tags: schema.maybe(schema.arrayOf(schema.string())),
'url.port': schema.maybe(schema.arrayOf(schema.string())),
}),
schema.string(),
])
),
// deprecated
locations: schema.maybe(schema.arrayOf(schema.string())),
numTimes: schema.number(),
search: schema.maybe(schema.string()),
shouldCheckStatus: schema.boolean(),
shouldCheckAvailability: schema.boolean(),
timerangeCount: schema.maybe(schema.number()),
timerangeUnit: schema.maybe(schema.string()),
// deprecated
timerange: schema.maybe(
schema.object({
from: schema.string(),
to: schema.string(),
})
),
version: schema.maybe(schema.number()),
isAutoGenerated: schema.maybe(schema.boolean()),
}),
},
defaultActionGroupId: MONITOR_STATUS.id,
actionGroups: [
{
id: MONITOR_STATUS.id,
name: MONITOR_STATUS.name,
},
defaultActionGroupId: MONITOR_STATUS.id,
actionGroups: [
],
actionVariables: {
context: [
{
id: MONITOR_STATUS.id,
name: MONITOR_STATUS.name,
name: 'message',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description',
{
defaultMessage: 'A generated message summarizing the currently down monitors',
}
),
},
{
name: 'downMonitorsWithGeo',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description',
{
defaultMessage:
'A generated summary that shows some or all of the monitors detected as "down" by the alert',
}
),
},
],
actionVariables: {
context: [
{
name: 'message',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description',
{
defaultMessage: 'A generated message summarizing the currently down monitors',
}
),
},
{
name: 'downMonitorsWithGeo',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description',
{
defaultMessage:
'A generated summary that shows some or all of the monitors detected as "down" by the alert',
}
),
},
],
state: [...commonMonitorStateI18, ...commonStateTranslations],
state: [...commonMonitorStateI18, ...commonStateTranslations],
},
isExportable: true,
minimumLicenseRequired: 'basic',
async executor({
params: rawParams,
state,
services: { savedObjectsClient, scopedClusterClient, alertWithLifecycle },
rule: {
schedule: { interval },
},
minimumLicenseRequired: 'basic',
isExportable: true,
async executor({
options: {
params: rawParams,
state,
services: { alertInstanceFactory },
rule: {
schedule: { interval },
},
},
uptimeEsClient,
}) {
const {
filters,
search,
}) {
const {
filters,
search,
numTimes,
timerangeCount,
timerangeUnit,
availability,
shouldCheckAvailability,
shouldCheckStatus,
isAutoGenerated,
timerange: oldVersionTimeRange,
} = rawParams;
const uptimeEsClient = createUptimeESClient({
esClient: scopedClusterClient.asCurrentUser,
savedObjectsClient,
});
const filterString = await formatFilterString(uptimeEsClient, filters, search, libs);
const timespanInterval = `${String(timerangeCount)}${timerangeUnit}`;
// Range filter for `monitor.timespan`, the range of time the ping is valid
const timespanRange = oldVersionTimeRange || {
from: `now-${timespanInterval}`,
to: 'now',
};
// Range filter for `@timestamp`, the time the document was indexed
const timestampRange = getTimestampRange({
ruleScheduleLookback: `now-${interval}`,
timerangeLookback: timespanRange.from,
});
let downMonitorsByLocation: GetMonitorStatusResult[] = [];
// if oldVersionTimeRange present means it's 7.7 format and
// after that shouldCheckStatus should be explicitly false
if (!(!oldVersionTimeRange && shouldCheckStatus === false)) {
downMonitorsByLocation = await libs.requests.getMonitorStatus({
uptimeEsClient,
timespanRange,
timestampRange,
numTimes,
timerangeCount,
timerangeUnit,
availability,
shouldCheckAvailability,
shouldCheckStatus,
isAutoGenerated,
timerange: oldVersionTimeRange,
} = rawParams;
const filterString = await formatFilterString(uptimeEsClient, filters, search, libs);
const timespanInterval = `${String(timerangeCount)}${timerangeUnit}`;
// Range filter for `monitor.timespan`, the range of time the ping is valid
const timespanRange = oldVersionTimeRange || {
from: `now-${timespanInterval}`,
to: 'now',
};
// Range filter for `@timestamp`, the time the document was indexed
const timestampRange = getTimestampRange({
ruleScheduleLookback: `now-${interval}`,
timerangeLookback: timespanRange.from,
locations: [],
filters: filterString,
});
}
let downMonitorsByLocation: GetMonitorStatusResult[] = [];
if (isAutoGenerated) {
for (const monitorLoc of downMonitorsByLocation) {
const monitorInfo = monitorLoc.monitorInfo;
// if oldVersionTimeRange present means it's 7.7 format and
// after that shouldCheckStatus should be explicitly false
if (!(!oldVersionTimeRange && shouldCheckStatus === false)) {
downMonitorsByLocation = await libs.requests.getMonitorStatus({
uptimeEsClient,
timespanRange,
timestampRange,
numTimes,
locations: [],
filters: filterString,
const statusMessage = getStatusMessage(monitorInfo);
const monitorSummary = getMonitorSummary(monitorInfo, statusMessage);
const alert = alertWithLifecycle({
id: getInstanceId(monitorInfo, monitorLoc.location),
fields: getMonitorAlertDocument(monitorSummary),
});
}
if (isAutoGenerated) {
for (const monitorLoc of downMonitorsByLocation) {
const monitorInfo = monitorLoc.monitorInfo;
const alertInstance = alertInstanceFactory(
getInstanceId(monitorInfo, monitorLoc.location)
);
const monitorSummary = getMonitorSummary(monitorInfo);
const statusMessage = getStatusMessage(monitorInfo);
alertInstance.replaceState({
...state,
...monitorSummary,
statusMessage,
...updateState(state, true),
});
alertInstance.scheduleActions(MONITOR_STATUS.id);
}
return updateState(state, downMonitorsByLocation.length > 0);
}
let availabilityResults: GetMonitorAvailabilityResult[] = [];
if (shouldCheckAvailability) {
availabilityResults = await libs.requests.getMonitorAvailability({
uptimeEsClient,
...availability,
filters: JSON.stringify(filterString) || undefined,
});
}
const mergedIdsByLoc = getUniqueIdsByLoc(downMonitorsByLocation, availabilityResults);
mergedIdsByLoc.forEach((monIdByLoc) => {
const availMonInfo = availabilityResults.find(
({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
);
const downMonInfo = downMonitorsByLocation.find(
({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
)?.monitorInfo;
const monitorInfo = downMonInfo || availMonInfo?.monitorInfo!;
const monitorSummary = getMonitorSummary(monitorInfo);
const statusMessage = getStatusMessage(downMonInfo!, availMonInfo!, availability);
const alertInstance = alertInstanceFactory(getInstanceId(monitorInfo, monIdByLoc));
alertInstance.replaceState({
...updateState(state, true),
alert.replaceState({
...state,
...monitorSummary,
statusMessage,
...updateState(state, true),
});
alertInstance.scheduleActions(MONITOR_STATUS.id, {
message: generateMessageForOlderVersions({ ...monitorSummary, statusMessage }),
});
alert.scheduleActions(MONITOR_STATUS.id);
}
return updateState(state, downMonitorsByLocation.length > 0);
}
let availabilityResults: GetMonitorAvailabilityResult[] = [];
if (shouldCheckAvailability) {
availabilityResults = await libs.requests.getMonitorAvailability({
uptimeEsClient,
...availability,
filters: JSON.stringify(filterString) || undefined,
});
}
const mergedIdsByLoc = getUniqueIdsByLoc(downMonitorsByLocation, availabilityResults);
mergedIdsByLoc.forEach((monIdByLoc) => {
const availMonInfo = availabilityResults.find(
({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
);
const downMonInfo = downMonitorsByLocation.find(
({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
)?.monitorInfo;
const monitorInfo = downMonInfo || availMonInfo?.monitorInfo!;
const statusMessage = getStatusMessage(downMonInfo!, availMonInfo!, availability);
const monitorSummary = getMonitorSummary(monitorInfo, statusMessage);
const alert = alertWithLifecycle({
id: getInstanceId(monitorInfo, monIdByLoc),
fields: getMonitorAlertDocument(monitorSummary),
});
return updateState(state, downMonitorsByLocation.length > 0);
},
});
alert.replaceState({
...updateState(state, true),
...monitorSummary,
statusMessage,
});
alert.scheduleActions(MONITOR_STATUS.id);
});
return updateState(state, downMonitorsByLocation.length > 0);
},
});

View file

@ -0,0 +1,81 @@
/*
* 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 { Logger } from 'kibana/server';
import { UMServerLibs } from '../../lib';
import { UptimeCorePlugins, UptimeCoreSetup } from '../../adapters';
import type { UptimeRouter } from '../../../types';
import type { RuleDataClient } from '../../../../../rule_registry/server';
import { getUptimeESMockClient } from '../../requests/helper';
import { alertsMock } from '../../../../../alerting/server/mocks';
import { DynamicSettings } from '../../../../common/runtime_types';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
/**
* The alert takes some dependencies as parameters; these are things like
* kibana core services and plugins. This function helps reduce the amount of
* boilerplate required.
* @param customRequests client tests can use this paramter to provide their own request mocks,
* so we don't have to mock them all for each test.
*/
export const bootstrapDependencies = (customRequests?: any, customPlugins: any = {}) => {
const router = {} as UptimeRouter;
// these server/libs parameters don't have any functionality, which is fine
// because we aren't testing them here
const server: UptimeCoreSetup = { router };
const plugins: UptimeCorePlugins = customPlugins as any;
const libs: UMServerLibs = { requests: {} } as UMServerLibs;
libs.requests = { ...libs.requests, ...customRequests };
return { server, libs, plugins };
};
export const createRuleTypeMocks = (
dynamicCertSettings: {
certAgeThreshold: DynamicSettings['certAgeThreshold'];
certExpirationThreshold: DynamicSettings['certExpirationThreshold'];
} = {
certAgeThreshold: DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
certExpirationThreshold: DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
}
) => {
const loggerMock = ({
debug: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
} as unknown) as Logger;
const scheduleActions = jest.fn();
const replaceState = jest.fn();
const services = {
...getUptimeESMockClient(),
...alertsMock.createAlertServices(),
alertWithLifecycle: jest.fn().mockReturnValue({ scheduleActions, replaceState }),
logger: loggerMock,
};
return {
dependencies: {
logger: loggerMock,
ruleDataClient: ({
getReader: () => {
return {
search: jest.fn(),
};
},
getWriter: () => {
return {
bulk: jest.fn(),
};
},
} as unknown) as RuleDataClient,
},
services,
scheduleActions,
replaceState,
};
};

View file

@ -4,52 +4,180 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment';
import { getCertSummary } from './tls';
import { Cert } from '../../../common/runtime_types';
import { tlsAlertFactory, getCertSummary, DEFAULT_SIZE } from './tls';
import { TLS } from '../../../common/constants/alerts';
import { CertResult, DynamicSettings } from '../../../common/runtime_types';
import { createRuleTypeMocks, bootstrapDependencies } from './test_utils';
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { savedObjectsAdapter, UMSavedObjectsAdapter } from '../saved_objects';
/**
* This function aims to provide an easy way to give mock props that will
* reduce boilerplate for tests.
* @param params the params received at alert creation time
* @param state the state the alert maintains
*/
const mockOptions = (
dynamicCertSettings?: {
certExpirationThreshold: DynamicSettings['certExpirationThreshold'];
certAgeThreshold: DynamicSettings['certAgeThreshold'];
},
state = {}
): any => {
const { services } = createRuleTypeMocks(dynamicCertSettings);
const params = {
timerange: { from: 'now-15m', to: 'now' },
};
return {
params,
state,
services,
};
};
const mockCertResult: CertResult = {
certs: [
{
not_after: '2020-07-16T03:15:39.000Z',
not_before: '2019-07-24T03:15:39.000Z',
issuer: 'Sample issuer',
common_name: 'Common-One',
monitors: [{ name: 'monitor-one', id: 'monitor1' }],
sha256: 'abc',
},
{
not_after: '2020-07-18T03:15:39.000Z',
not_before: '2019-07-20T03:15:39.000Z',
issuer: 'Sample issuer',
common_name: 'Common-Two',
monitors: [{ name: 'monitor-two', id: 'monitor2' }],
sha256: 'bcd',
},
{
not_after: '2020-07-19T03:15:39.000Z',
not_before: '2019-07-22T03:15:39.000Z',
issuer: 'Sample issuer',
common_name: 'Common-Three',
monitors: [{ name: 'monitor-three', id: 'monitor3' }],
sha256: 'cde',
},
{
not_after: '2020-07-25T03:15:39.000Z',
not_before: '2019-07-25T03:15:39.000Z',
issuer: 'Sample issuer',
common_name: 'Common-Four',
monitors: [{ name: 'monitor-four', id: 'monitor4' }],
sha256: 'def',
},
],
total: 4,
};
describe('tls alert', () => {
let toISOStringSpy: jest.SpyInstance<string, []>;
let savedObjectsAdapterSpy: jest.SpyInstance<
ReturnType<UMSavedObjectsAdapter['getUptimeDynamicSettings']>
>;
const mockDate = 'date';
beforeAll(() => {
Date.now = jest.fn().mockReturnValue(new Date('2021-05-13T12:33:37.000Z'));
});
describe('alert executor', () => {
beforeEach(() => {
toISOStringSpy = jest.spyOn(Date.prototype, 'toISOString');
savedObjectsAdapterSpy = jest.spyOn(savedObjectsAdapter, 'getUptimeDynamicSettings');
});
it('triggers when aging or expiring alerts are found', async () => {
toISOStringSpy.mockImplementation(() => mockDate);
const mockGetter: jest.Mock<CertResult> = jest.fn();
mockGetter.mockReturnValue(mockCertResult);
const { server, libs, plugins } = bootstrapDependencies({ getCerts: mockGetter });
const alert = tlsAlertFactory(server, libs, plugins);
const options = mockOptions();
const {
services: { alertWithLifecycle },
} = options;
await alert.executor(options);
expect(mockGetter).toHaveBeenCalledTimes(1);
expect(alertWithLifecycle).toHaveBeenCalledTimes(4);
mockCertResult.certs.forEach((cert) => {
expect(alertWithLifecycle).toBeCalledWith({
fields: expect.objectContaining({
'tls.server.x509.subject.common_name': cert.common_name,
'tls.server.x509.issuer.common_name': cert.issuer,
'tls.server.x509.not_after': cert.not_after,
'tls.server.x509.not_before': cert.not_before,
'tls.server.hash.sha256': cert.sha256,
}),
id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`,
});
});
expect(mockGetter).toBeCalledWith(
expect.objectContaining({
from: DEFAULT_FROM,
to: DEFAULT_TO,
index: 0,
size: DEFAULT_SIZE,
notValidAfter: `now+${DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold}d`,
notValidBefore: `now-${DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold}d`,
sortBy: 'common_name',
direction: 'desc',
})
);
const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results;
expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(4);
mockCertResult.certs.forEach((cert) => {
expect(alertInstanceMock.replaceState).toBeCalledWith(
expect.objectContaining({
commonName: cert.common_name,
issuer: cert.issuer,
status: 'expired',
})
);
});
expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(4);
expect(alertInstanceMock.scheduleActions).toBeCalledWith(TLS.id);
});
it('handles dynamic settings for aging or expiration threshold', async () => {
toISOStringSpy.mockImplementation(() => mockDate);
const certSettings = {
certAgeThreshold: 10,
certExpirationThreshold: 5,
heartbeatIndices: 'heartbeat-*',
defaultConnectors: [],
};
savedObjectsAdapterSpy.mockImplementation(() => certSettings);
const mockGetter: jest.Mock<CertResult> = jest.fn();
mockGetter.mockReturnValue(mockCertResult);
const { server, libs, plugins } = bootstrapDependencies({ getCerts: mockGetter });
const alert = tlsAlertFactory(server, libs, plugins);
const options = mockOptions();
await alert.executor(options);
expect(mockGetter).toHaveBeenCalledTimes(1);
expect(mockGetter).toBeCalledWith(
expect.objectContaining({
notValidAfter: `now+${certSettings.certExpirationThreshold}d`,
notValidBefore: `now-${certSettings.certAgeThreshold}d`,
})
);
});
});
describe('getCertSummary', () => {
let mockCerts: Cert[];
let diffSpy: jest.SpyInstance<any, unknown[]>;
beforeEach(() => {
diffSpy = jest.spyOn(moment.prototype, 'diff');
mockCerts = [
{
not_after: '2020-07-16T03:15:39.000Z',
not_before: '2019-07-24T03:15:39.000Z',
common_name: 'Common-One',
monitors: [{ name: 'monitor-one', id: 'monitor1' }],
sha256: 'abc',
issuer: 'Cloudflare Inc ECC CA-3',
},
{
not_after: '2020-07-18T03:15:39.000Z',
not_before: '2019-07-20T03:15:39.000Z',
common_name: 'Common-Two',
monitors: [{ name: 'monitor-two', id: 'monitor2' }],
sha256: 'bcd',
issuer: 'Cloudflare Inc ECC CA-3',
},
{
not_after: '2020-07-19T03:15:39.000Z',
not_before: '2019-07-22T03:15:39.000Z',
common_name: 'Common-Three',
monitors: [{ name: 'monitor-three', id: 'monitor3' }],
sha256: 'cde',
issuer: 'Cloudflare Inc ECC CA-3',
},
{
not_after: '2020-07-25T03:15:39.000Z',
not_before: '2019-07-25T03:15:39.000Z',
common_name: 'Common-Four',
monitors: [{ name: 'monitor-four', id: 'monitor4' }],
sha256: 'def',
issuer: 'Cloudflare Inc ECC CA-3',
},
];
});
afterEach(() => {
@ -59,13 +187,13 @@ describe('tls alert', () => {
it('handles positive diffs for expired certs appropriately', () => {
diffSpy.mockReturnValueOnce(900);
const result = getCertSummary(
mockCerts[0],
mockCertResult.certs[0],
new Date('2020-07-20T05:00:00.000Z').valueOf(),
new Date('2019-03-01T00:00:00.000Z').valueOf()
);
expect(result).toEqual({
commonName: mockCerts[0].common_name,
issuer: mockCerts[0].issuer,
commonName: mockCertResult.certs[0].common_name,
issuer: mockCertResult.certs[0].issuer,
summary: 'expired on Jul 15, 2020 EDT, 900 days ago.',
status: 'expired',
});
@ -74,13 +202,13 @@ describe('tls alert', () => {
it('handles positive diffs for agining certs appropriately', () => {
diffSpy.mockReturnValueOnce(702);
const result = getCertSummary(
mockCerts[0],
mockCertResult.certs[0],
new Date('2020-07-01T12:00:00.000Z').valueOf(),
new Date('2019-09-01T03:00:00.000Z').valueOf()
);
expect(result).toEqual({
commonName: mockCerts[0].common_name,
issuer: mockCerts[0].issuer,
commonName: mockCertResult.certs[0].common_name,
issuer: mockCertResult.certs[0].issuer,
summary: 'valid since Jul 23, 2019 EDT, 702 days ago.',
status: 'becoming too old',
});
@ -89,13 +217,13 @@ describe('tls alert', () => {
it('handles negative diff values appropriately for aging certs', () => {
diffSpy.mockReturnValueOnce(-90);
const result = getCertSummary(
mockCerts[0],
mockCertResult.certs[0],
new Date('2020-07-01T12:00:00.000Z').valueOf(),
new Date('2019-09-01T03:00:00.000Z').valueOf()
);
expect(result).toEqual({
commonName: mockCerts[0].common_name,
issuer: mockCerts[0].issuer,
commonName: mockCertResult.certs[0].common_name,
issuer: mockCertResult.certs[0].issuer,
summary: 'invalid until Jul 23, 2019 EDT, 90 days from now.',
status: 'invalid',
});
@ -106,13 +234,13 @@ describe('tls alert', () => {
// negative days are in the future, positive days are in the past
.mockReturnValueOnce(-96);
const result = getCertSummary(
mockCerts[0],
mockCertResult.certs[0],
new Date('2020-07-20T05:00:00.000Z').valueOf(),
new Date('2019-03-01T00:00:00.000Z').valueOf()
);
expect(result).toEqual({
commonName: mockCerts[0].common_name,
issuer: mockCerts[0].issuer,
commonName: mockCertResult.certs[0].common_name,
issuer: mockCertResult.certs[0].issuer,
summary: 'expires on Jul 15, 2020 EDT in 96 days.',
status: 'expiring',
});

View file

@ -8,18 +8,22 @@
import moment from 'moment';
import { schema } from '@kbn/config-schema';
import { UptimeAlertTypeFactory } from './types';
import { updateState } from './common';
import { updateState, generateAlertMessage } from './common';
import { TLS } from '../../../common/constants/alerts';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { Cert, CertResult } from '../../../common/runtime_types';
import { commonStateTranslations, tlsTranslations } from './translations';
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
import { uptimeAlertWrapper } from './uptime_alert_wrapper';
import { TlsTranslations } from '../../../common/translations';
import { ActionGroupIdsOf } from '../../../../alerting/common';
import { savedObjectsAdapter } from '../saved_objects';
import { createUptimeESClient } from '../lib';
export type ActionGroupIds = ActionGroupIdsOf<typeof TLS>;
const DEFAULT_SIZE = 20;
export const DEFAULT_SIZE = 20;
interface TlsAlertState {
commonName: string;
@ -93,78 +97,92 @@ export const getCertSummary = (
};
};
export const tlsAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) =>
uptimeAlertWrapper<ActionGroupIds>({
id: 'xpack.uptime.alerts.tlsCertificate',
name: tlsTranslations.alertFactoryName,
validate: {
params: schema.object({}),
export const tlsAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) => ({
id: 'xpack.uptime.alerts.tlsCertificate',
producer: 'uptime',
name: tlsTranslations.alertFactoryName,
validate: {
params: schema.object({}),
},
defaultActionGroupId: TLS.id,
actionGroups: [
{
id: TLS.id,
name: TLS.name,
},
defaultActionGroupId: TLS.id,
actionGroups: [
{
id: TLS.id,
name: TLS.name,
},
],
actionVariables: {
context: [],
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
},
minimumLicenseRequired: 'basic',
isExportable: true,
async executor({ options, dynamicSettings, uptimeEsClient }) {
const {
services: { alertInstanceFactory },
state,
} = options;
],
actionVariables: {
context: [],
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
},
isExportable: true,
minimumLicenseRequired: 'basic',
async executor({
services: { alertWithLifecycle, savedObjectsClient, scopedClusterClient },
state,
}) {
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
const { certs, total }: CertResult = await libs.requests.getCerts({
uptimeEsClient,
from: DEFAULT_FROM,
to: DEFAULT_TO,
index: 0,
size: DEFAULT_SIZE,
notValidAfter: `now+${
dynamicSettings?.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
}d`,
notValidBefore: `now-${
dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
}d`,
sortBy: 'common_name',
direction: 'desc',
});
const uptimeEsClient = createUptimeESClient({
esClient: scopedClusterClient.asCurrentUser,
savedObjectsClient,
});
const foundCerts = total > 0;
const { certs, total }: CertResult = await libs.requests.getCerts({
uptimeEsClient,
from: DEFAULT_FROM,
to: DEFAULT_TO,
index: 0,
size: DEFAULT_SIZE,
notValidAfter: `now+${
dynamicSettings?.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
}d`,
notValidBefore: `now-${
dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
}d`,
sortBy: 'common_name',
direction: 'desc',
});
if (foundCerts) {
certs.forEach((cert) => {
const absoluteExpirationThreshold = moment()
.add(
dynamicSettings.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
'd'
)
.valueOf();
const absoluteAgeThreshold = moment()
.subtract(
dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
'd'
)
.valueOf();
const alertInstance = alertInstanceFactory(
`${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`
);
const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold);
alertInstance.replaceState({
...updateState(state, foundCerts),
...summary,
});
alertInstance.scheduleActions(TLS.id);
const foundCerts = total > 0;
if (foundCerts) {
certs.forEach((cert) => {
const absoluteExpirationThreshold = moment()
.add(
dynamicSettings.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
'd'
)
.valueOf();
const absoluteAgeThreshold = moment()
.subtract(
dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
'd'
)
.valueOf();
const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold);
const alertInstance = alertWithLifecycle({
id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`,
fields: {
'tls.server.x509.subject.common_name': cert.common_name,
'tls.server.x509.issuer.common_name': cert.issuer,
'tls.server.x509.not_after': cert.not_after,
'tls.server.x509.not_before': cert.not_before,
'tls.server.hash.sha256': cert.sha256,
reason: generateAlertMessage(TlsTranslations.defaultActionMessage, summary),
},
});
}
alertInstance.replaceState({
...updateState(state, foundCerts),
...summary,
});
alertInstance.scheduleActions(TLS.id);
});
}
return updateState(state, foundCerts);
},
});
return updateState(state, foundCerts);
},
});

View file

@ -14,11 +14,18 @@ import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { Cert, CertResult } from '../../../common/runtime_types';
import { commonStateTranslations, tlsTranslations } from './translations';
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
import { uptimeAlertWrapper } from './uptime_alert_wrapper';
import { ActionGroupIdsOf } from '../../../../alerting/common';
import { AlertInstanceContext } from '../../../../alerting/common';
import { AlertInstance } from '../../../../alerting/server';
import { savedObjectsAdapter } from '../saved_objects';
import { createUptimeESClient } from '../lib';
export type ActionGroupIds = ActionGroupIdsOf<typeof TLS_LEGACY>;
type TLSAlertInstance = AlertInstance<Record<string, any>, AlertInstanceContext, ActionGroupIds>;
const DEFAULT_SIZE = 20;
interface TlsAlertState {
@ -84,74 +91,78 @@ export const getCertSummary = (
};
};
export const tlsLegacyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) =>
uptimeAlertWrapper<ActionGroupIds>({
id: 'xpack.uptime.alerts.tls',
name: tlsTranslations.legacyAlertFactoryName,
validate: {
params: schema.object({}),
export const tlsLegacyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) => ({
id: 'xpack.uptime.alerts.tls',
producer: 'uptime',
name: tlsTranslations.legacyAlertFactoryName,
validate: {
params: schema.object({}),
},
defaultActionGroupId: TLS_LEGACY.id,
actionGroups: [
{
id: TLS_LEGACY.id,
name: TLS_LEGACY.name,
},
defaultActionGroupId: TLS_LEGACY.id,
actionGroups: [
{
id: TLS_LEGACY.id,
name: TLS_LEGACY.name,
},
],
actionVariables: {
context: [],
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
},
minimumLicenseRequired: 'basic',
isExportable: true,
async executor({ options, dynamicSettings, uptimeEsClient }) {
const {
services: { alertInstanceFactory },
state,
} = options;
],
actionVariables: {
context: [],
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
},
isExportable: true,
minimumLicenseRequired: 'basic',
async executor({
services: { alertInstanceFactory, scopedClusterClient, savedObjectsClient },
state,
}) {
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
const { certs, total }: CertResult = await libs.requests.getCerts({
uptimeEsClient,
from: DEFAULT_FROM,
to: DEFAULT_TO,
index: 0,
size: DEFAULT_SIZE,
notValidAfter: `now+${
dynamicSettings?.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
}d`,
notValidBefore: `now-${
dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
}d`,
sortBy: 'common_name',
direction: 'desc',
const uptimeEsClient = createUptimeESClient({
esClient: scopedClusterClient.asCurrentUser,
savedObjectsClient,
});
const { certs, total }: CertResult = await libs.requests.getCerts({
uptimeEsClient,
from: DEFAULT_FROM,
to: DEFAULT_TO,
index: 0,
size: DEFAULT_SIZE,
notValidAfter: `now+${
dynamicSettings?.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
}d`,
notValidBefore: `now-${
dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
}d`,
sortBy: 'common_name',
direction: 'desc',
});
const foundCerts = total > 0;
if (foundCerts) {
const absoluteExpirationThreshold = moment()
.add(
dynamicSettings.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
'd'
)
.valueOf();
const absoluteAgeThreshold = moment()
.subtract(
dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
'd'
)
.valueOf();
const alertInstance: TLSAlertInstance = alertInstanceFactory(TLS_LEGACY.id);
const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
alertInstance.replaceState({
...updateState(state, foundCerts),
...summary,
});
alertInstance.scheduleActions(TLS_LEGACY.id);
}
const foundCerts = total > 0;
if (foundCerts) {
const absoluteExpirationThreshold = moment()
.add(
dynamicSettings.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
'd'
)
.valueOf();
const absoluteAgeThreshold = moment()
.subtract(
dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
'd'
)
.valueOf();
const alertInstance = alertInstanceFactory(TLS_LEGACY.id);
const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
alertInstance.replaceState({
...updateState(state, foundCerts),
...summary,
});
alertInstance.scheduleActions(TLS_LEGACY.id);
}
return updateState(state, foundCerts);
},
});
return updateState(state, foundCerts);
},
});

View file

@ -4,21 +4,27 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters';
import { UMServerLibs } from '../lib';
import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../alerting/server';
import { AlertTypeWithExecutor, LifecycleAlertService } from '../../../../rule_registry/server';
import { AlertInstanceContext } from '../../../../alerting/common';
export type UptimeAlertTypeParam = Record<string, any>;
export type UptimeAlertTypeState = Record<string, any>;
export type UptimeAlertTypeFactory<ActionGroupIds extends string> = (
/**
* Because all of our types are presumably going to list the `producer` as `'uptime'`,
* we should just omit this field from the returned value to simplify the returned alert type.
*
* When we register all the alerts we can inject this field.
*/
export type DefaultUptimeAlertInstance<TActionGroupIds extends string> = AlertTypeWithExecutor<
Record<string, any>,
AlertInstanceContext,
{
alertWithLifecycle: LifecycleAlertService<AlertInstanceContext, TActionGroupIds>;
}
>;
export type UptimeAlertTypeFactory<TActionGroupIds extends string> = (
server: UptimeCoreSetup,
libs: UMServerLibs,
plugins: UptimeCorePlugins
) => AlertType<
UptimeAlertTypeParam,
UptimeAlertTypeState,
AlertInstanceState,
AlertInstanceContext,
ActionGroupIds
>;
) => DefaultUptimeAlertInstance<TActionGroupIds>;

View file

@ -1,68 +0,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 { SavedObjectsClientContract } from 'kibana/server';
import {
AlertExecutorOptions,
AlertInstanceState,
AlertInstanceContext,
} from '../../../../alerting/server';
import { savedObjectsAdapter } from '../saved_objects';
import { DynamicSettings } from '../../../common/runtime_types';
import { createUptimeESClient, UptimeESClient } from '../lib';
import { UptimeAlertTypeFactory, UptimeAlertTypeParam, UptimeAlertTypeState } from './types';
export interface UptimeAlertType<ActionGroupIds extends string>
extends Omit<ReturnType<UptimeAlertTypeFactory<ActionGroupIds>>, 'executor' | 'producer'> {
executor: ({
options,
uptimeEsClient,
dynamicSettings,
}: {
options: AlertExecutorOptions<
UptimeAlertTypeParam,
UptimeAlertTypeState,
AlertInstanceState,
AlertInstanceContext,
ActionGroupIds
>;
uptimeEsClient: UptimeESClient;
dynamicSettings: DynamicSettings;
savedObjectsClient: SavedObjectsClientContract;
}) => Promise<UptimeAlertTypeState | void>;
}
export const uptimeAlertWrapper = <ActionGroupIds extends string>(
uptimeAlert: UptimeAlertType<ActionGroupIds>
) => ({
...uptimeAlert,
producer: 'uptime',
executor: async (
options: AlertExecutorOptions<
UptimeAlertTypeParam,
UptimeAlertTypeState,
AlertInstanceState,
AlertInstanceContext,
ActionGroupIds
>
) => {
const {
services: { scopedClusterClient: esClient, savedObjectsClient },
} = options;
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(
options.services.savedObjectsClient
);
const uptimeEsClient = createUptimeESClient({
esClient: esClient.asCurrentUser,
savedObjectsClient,
});
return uptimeAlert.executor({ options, dynamicSettings, uptimeEsClient, savedObjectsClient });
},
});

View file

@ -4,30 +4,97 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { once } from 'lodash';
import {
PluginInitializerContext,
CoreStart,
CoreSetup,
Plugin as PluginType,
ISavedObjectsRepository,
Logger,
} from '../../../../src/core/server';
import { uptimeRuleFieldMap } from '../common/rules/uptime_rule_field_map';
import { initServerWithKibana } from './kibana.index';
import { KibanaTelemetryAdapter, UptimeCorePlugins } from './lib/adapters';
import { umDynamicSettings } from './lib/saved_objects';
import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map';
import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets';
export type UptimeRuleRegistry = ReturnType<Plugin['setup']>['ruleRegistry'];
export class Plugin implements PluginType {
private savedObjectsClient?: ISavedObjectsRepository;
private initContext: PluginInitializerContext;
private logger?: Logger;
constructor(_initializerContext: PluginInitializerContext) {}
constructor(_initializerContext: PluginInitializerContext) {
this.initContext = _initializerContext;
}
public setup(core: CoreSetup, plugins: UptimeCorePlugins) {
initServerWithKibana({ router: core.http.createRouter() }, plugins);
this.logger = this.initContext.logger.get();
const { ruleDataService } = plugins.ruleRegistry;
const ready = once(async () => {
const componentTemplateName = ruleDataService.getFullAssetName('synthetics-mappings');
const alertsIndexPattern = ruleDataService.getFullAssetName('observability.synthetics*');
if (!ruleDataService.isWriteEnabled()) {
return;
}
await ruleDataService.createOrUpdateComponentTemplate({
name: componentTemplateName,
body: {
template: {
settings: {
number_of_shards: 1,
},
mappings: mappingFromFieldMap(uptimeRuleFieldMap),
},
},
});
await ruleDataService.createOrUpdateIndexTemplate({
name: ruleDataService.getFullAssetName('synthetics-index-template'),
body: {
index_patterns: [alertsIndexPattern],
composed_of: [
ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME),
componentTemplateName,
],
},
});
await ruleDataService.updateIndexMappingsMatchingPattern(alertsIndexPattern);
});
// initialize eagerly
const initializeRuleDataTemplatesPromise = ready().catch((err) => {
this.logger!.error(err);
});
const ruleDataClient = ruleDataService.getRuleDataClient(
'synthetics',
ruleDataService.getFullAssetName('observability.synthetics'),
() => initializeRuleDataTemplatesPromise
);
initServerWithKibana(
{ router: core.http.createRouter() },
plugins,
ruleDataClient,
this.logger
);
core.savedObjects.registerType(umDynamicSettings);
KibanaTelemetryAdapter.registerUsageCollector(
plugins.usageCollection,
() => this.savedObjectsClient
);
return {
ruleRegistry: ruleDataClient,
};
}
public start(core: CoreStart, _plugins: any) {

View file

@ -5,21 +5,47 @@
* 2.0.
*/
import { Logger } from 'kibana/server';
import { createLifecycleRuleTypeFactory, RuleDataClient } from '../../rule_registry/server';
import { UMServerLibs } from './lib/lib';
import { createRouteWithAuth, restApiRoutes, uptimeRouteWrapper } from './rest_api';
import { UptimeCoreSetup, UptimeCorePlugins } from './lib/adapters';
import { uptimeAlertTypeFactories } from './lib/alerts';
import { statusCheckAlertFactory } from './lib/alerts/status_check';
import { tlsAlertFactory } from './lib/alerts/tls';
import { tlsLegacyAlertFactory } from './lib/alerts/tls_legacy';
import { durationAnomalyAlertFactory } from './lib/alerts/duration_anomaly';
export const initUptimeServer = (
server: UptimeCoreSetup,
libs: UMServerLibs,
plugins: UptimeCorePlugins
plugins: UptimeCorePlugins,
ruleDataClient: RuleDataClient,
logger: Logger
) => {
restApiRoutes.forEach((route) =>
libs.framework.registerRoute(uptimeRouteWrapper(createRouteWithAuth(libs, route)))
);
uptimeAlertTypeFactories.forEach((alertTypeFactory) =>
plugins.alerting.registerType(alertTypeFactory(server, libs, plugins))
);
const {
alerting: { registerType },
} = plugins;
const statusAlert = statusCheckAlertFactory(server, libs, plugins);
const tlsLegacyAlert = tlsLegacyAlertFactory(server, libs, plugins);
const tlsAlert = tlsAlertFactory(server, libs, plugins);
const durationAlert = durationAnomalyAlertFactory(server, libs, plugins);
const createLifecycleRuleType = createLifecycleRuleTypeFactory({
ruleDataClient,
logger,
});
registerType(createLifecycleRuleType(statusAlert));
registerType(createLifecycleRuleType(tlsAlert));
registerType(createLifecycleRuleType(durationAlert));
/* TLS Legacy rule supported at least through 8.0.
* Not registered with RAC */
registerType(tlsLegacyAlert);
};