[Synthetics] Add TLS Certificate expiry alert (#159697)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Abdul Wahab Zahid <awahab07@yahoo.com>
This commit is contained in:
Shahzad 2023-06-20 12:24:06 +02:00 committed by GitHub
parent 97dc2ecba1
commit 8d83f64383
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1635 additions and 94 deletions

View file

@ -1211,7 +1211,7 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/
| Deprecated API | Reference location(s) | Remove By |
| ---------------|-----------|-----------|
| <DocLink id="kibAlertingPluginApi" section="def-server.RuleExecutorServices.alertFactory" text="alertFactory"/> | [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/common.ts#:~:text=alertFactory), [status_check.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/status_check.ts#:~:text=alertFactory), [tls.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/tls.ts#:~:text=alertFactory), [tls_legacy.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/tls_legacy.ts#:~:text=alertFactory), [duration_anomaly.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/duration_anomaly.ts#:~:text=alertFactory), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/alert_rules/common.ts#:~:text=alertFactory), [monitor_status_rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts#:~:text=alertFactory) | - |
| <DocLink id="kibAlertingPluginApi" section="def-server.RuleExecutorServices.alertFactory" text="alertFactory"/> | [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/common.ts#:~:text=alertFactory), [status_check.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/status_check.ts#:~:text=alertFactory), [tls.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/tls.ts#:~:text=alertFactory), [tls_legacy.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/tls_legacy.ts#:~:text=alertFactory), [duration_anomaly.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/duration_anomaly.ts#:~:text=alertFactory), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/alert_rules/common.ts#:~:text=alertFactory), [tls_rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/alert_rules/status_rule/tls_rule.ts#:~:text=alertFactory) | - |
| <DocLink id="kibDataPluginApi" section="def-common.DataView.title" text="title"/> | [filter_group.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx#:~:text=title), [filters_expression_select.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx#:~:text=title), [filter_group.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx#:~:text=title), [filters_expression_select.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx#:~:text=title) | - |
| <DocLink id="kibDataPluginApi" section="def-server.DataView.title" text="title"/> | [filter_group.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx#:~:text=title), [filters_expression_select.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx#:~:text=title), [filter_group.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx#:~:text=title), [filters_expression_select.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx#:~:text=title) | - |
| <DocLink id="kibDataViewsPluginApi" section="def-public.DataView.title" text="title"/> | [filter_group.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx#:~:text=title), [filters_expression_select.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx#:~:text=title) | - |

View file

@ -931,17 +931,6 @@
}
}
},
"event-annotation-group": {
"dynamic": false,
"properties": {
"title": {
"type": "text"
},
"description": {
"type": "text"
}
}
},
"visualization": {
"dynamic": false,
"properties": {
@ -959,6 +948,17 @@
}
}
},
"event-annotation-group": {
"dynamic": false,
"properties": {
"title": {
"type": "text"
},
"description": {
"type": "text"
}
}
},
"dashboard": {
"properties": {
"description": {
@ -2377,6 +2377,13 @@
"type": "boolean"
}
}
},
"tls": {
"properties": {
"enabled": {
"type": "boolean"
}
}
}
}
},

View file

@ -140,7 +140,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"slo": "2048ab6791df2e1ae0936f29c20765cb8d2fcfaa",
"space": "8de4ec513e9bbc6b2f1d635161d850be7747d38e",
"spaces-usage-stats": "3abca98713c52af8b30300e386c7779b3025a20e",
"synthetics-monitor": "ca7c0710c0607e44b2c52e5a41086b8b4a214f63",
"synthetics-monitor": "33ddc4b8979f378edf58bcc7ba13c5c5b572f42d",
"synthetics-param": "3ebb744e5571de678b1312d5c418c8188002cf5e",
"synthetics-privates-locations": "9cfbd6d1d2e2c559bf96dd6fbc27ff0c47719dd3",
"tag": "e2544392fe6563e215bb677abc8b01c2601ef2dc",

View file

@ -132,7 +132,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = {
[ConfigKey.MONITOR_TYPE]: DataStream.HTTP,
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
[ConfigKey.ENABLED]: true,
[ConfigKey.ALERT_CONFIG]: { status: { enabled: true } },
[ConfigKey.ALERT_CONFIG]: { status: { enabled: true }, tls: { enabled: true } },
[ConfigKey.SCHEDULE]: {
number: '3',
unit: ScheduleUnit.MINUTES,

View file

@ -8,7 +8,7 @@
export enum SYNTHETICS_API_URLS {
// Service end points
INDEX_TEMPLATES = '/internal/synthetics/service/index_templates',
SERVICE_LOCATIONS = '/internal/synthetics/service/locations',
SERVICE_LOCATIONS = '/internal/uptime/service/locations',
SYNTHETICS_MONITORS = '/internal/synthetics/service/monitors',
SYNTHETICS_MONITOR_INSPECT = '/internal/synthetics/service/monitor/inspect',
GET_SYNTHETICS_MONITOR = '/internal/synthetics/service/monitor/{monitorId}',

View file

@ -9,7 +9,8 @@ import { ActionGroup } from '@kbn/alerting-plugin/common';
import { i18n } from '@kbn/i18n';
export type MonitorStatusActionGroup =
ActionGroup<'xpack.synthetics.alerts.actionGroups.monitorStatus'>;
| ActionGroup<'xpack.synthetics.alerts.actionGroups.monitorStatus'>
| ActionGroup<'xpack.synthetics.alerts.actionGroups.tls'>;
export const MONITOR_STATUS: MonitorStatusActionGroup = {
id: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
@ -18,18 +19,29 @@ export const MONITOR_STATUS: MonitorStatusActionGroup = {
}),
};
export const TLS_CERTIFICATE: MonitorStatusActionGroup = {
id: 'xpack.synthetics.alerts.actionGroups.tls',
name: i18n.translate('xpack.synthetics.alertRules.actionGroups.tls', {
defaultMessage: 'Synthetics TLS certificate',
}),
};
export const ACTION_GROUP_DEFINITIONS: {
MONITOR_STATUS: MonitorStatusActionGroup;
TLS_CERTIFICATE: MonitorStatusActionGroup;
} = {
MONITOR_STATUS,
TLS_CERTIFICATE,
};
export const SYNTHETICS_STATUS_RULE = 'xpack.synthetics.alerts.monitorStatus';
export const SYNTHETICS_TLS_RULE = 'xpack.synthetics.alerts.tls';
export const SYNTHETICS_ALERT_RULE_TYPES = {
MONITOR_STATUS: SYNTHETICS_STATUS_RULE,
TLS: SYNTHETICS_TLS_RULE,
};
export const SYNTHETICS_RULE_TYPES = [SYNTHETICS_STATUS_RULE];
export const SYNTHETICS_RULE_TYPES = [SYNTHETICS_STATUS_RULE, SYNTHETICS_TLS_RULE];
export const SYNTHETICS_RULE_TYPES_ALERT_CONTEXT = 'observability.uptime';

View file

@ -34,18 +34,6 @@ export const DURATION_ANOMALY: DurationAnomalyActionGroup = {
name: 'Uptime Duration Anomaly',
};
export const ACTION_GROUP_DEFINITIONS: {
MONITOR_STATUS: MonitorStatusActionGroup;
TLS_LEGACY: TLSLegacyActionGroup;
TLS: TLSActionGroup;
DURATION_ANOMALY: DurationAnomalyActionGroup;
} = {
MONITOR_STATUS,
TLS_LEGACY,
TLS,
DURATION_ANOMALY,
};
export const CLIENT_ALERT_TYPES = {
MONITOR_STATUS: 'xpack.uptime.alerts.monitorStatus',
TLS_LEGACY: 'xpack.uptime.alerts.tls',

View file

@ -6,16 +6,17 @@
*/
export const AGENT_NAME = 'agent.name';
export const MONITOR_ID = 'monitor.id';
export const MONITOR_NAME = 'monitor.name';
export const MONITOR_TYPE = 'monitor.type';
export const URL_FULL = 'url.full';
export const URL_PORT = 'url.port';
export const OBSERVER_GEO_NAME = 'observer.geo.name';
export const ERROR_MESSAGE = 'error.message';
export const STATE_ID = 'monitor.state.id';
export const STATE_ID = 'montior.state.id';
export const CERT_COMMON_NAME = 'tls.server.x509.subject.common_name';
export const CERT_ISSUER_NAME = 'tls.server.x509.issuer.common_name';
export const CERT_VALID_NOT_AFTER = 'tls.server.x509.not_after';
export const CERT_VALID_NOT_BEFORE = 'tls.server.x509.not_before';
export const CERT_HASH_SHA256 = 'tls.server.hash.sha256';

View file

@ -31,6 +31,7 @@ function absoluteDate(relativeDate: string) {
}
export const getCertsRequestBody = ({
monitorIds,
pageIndex,
search,
notValidBefore,
@ -79,6 +80,9 @@ export const getCertsRequestBody = ({
: {}),
filter: [
...(filters ? [filters] : []),
...(monitorIds && monitorIds.length > 0
? [{ terms: { 'monitor.id': monitorIds } }]
: []),
{
exists: {
field: 'tls.server.hash.sha256',
@ -129,6 +133,9 @@ export const getCertsRequestBody = ({
_source: [
'monitor.id',
'monitor.name',
'monitor.type',
'url.full',
'observer.geo.name',
'tls.server.x509.issuer.common_name',
'tls.server.x509.subject.common_name',
'tls.server.hash.sha1',
@ -193,6 +200,10 @@ export const processCertsResult = (result: CertificatesResults): CertResult => {
not_after: notAfter,
not_before: notBefore,
common_name: commonName,
monitorName: ping?.monitor?.name,
monitorUrl: ping?.url?.full,
monitorType: ping?.monitor?.type,
locationName: ping?.observer?.geo?.name,
};
});
const total = result.aggregations?.total?.value ?? 0;

View file

@ -211,6 +211,7 @@ describe('Legacy Alert Actions factory', () => {
defaultSubjectMessage: MonitorStatusTranslations.defaultSubjectMessage,
defaultRecoverySubjectMessage: MonitorStatusTranslations.defaultRecoverySubjectMessage,
},
isLegacy: true,
});
expect(resp).toEqual([
{
@ -243,6 +244,11 @@ describe('Alert Actions factory', () => {
groupId: SYNTHETICS_MONITOR_STATUS.id,
defaultActions: [
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
actionTypeId: '.pagerduty',
group: 'xpack.uptime.alerts.actionGroups.monitorStatus',
params: {
@ -264,6 +270,11 @@ describe('Alert Actions factory', () => {
});
expect(resp).toEqual([
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
group: 'recovered',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@ -274,6 +285,11 @@ describe('Alert Actions factory', () => {
},
},
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@ -291,6 +307,11 @@ describe('Alert Actions factory', () => {
groupId: SYNTHETICS_MONITOR_STATUS.id,
defaultActions: [
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
actionTypeId: '.index',
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
params: {
@ -312,6 +333,11 @@ describe('Alert Actions factory', () => {
});
expect(resp).toEqual([
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
group: 'recovered',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@ -329,6 +355,11 @@ describe('Alert Actions factory', () => {
},
},
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@ -352,6 +383,11 @@ describe('Alert Actions factory', () => {
groupId: SYNTHETICS_MONITOR_STATUS.id,
defaultActions: [
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
actionTypeId: '.pagerduty',
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
params: {
@ -374,6 +410,11 @@ describe('Alert Actions factory', () => {
});
expect(resp).toEqual([
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
group: 'recovered',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@ -384,6 +425,11 @@ describe('Alert Actions factory', () => {
},
},
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@ -401,6 +447,11 @@ describe('Alert Actions factory', () => {
groupId: SYNTHETICS_MONITOR_STATUS.id,
defaultActions: [
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
actionTypeId: '.email',
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
params: {
@ -426,6 +477,11 @@ describe('Alert Actions factory', () => {
});
expect(resp).toEqual([
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
group: 'recovered',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@ -444,6 +500,11 @@ describe('Alert Actions factory', () => {
},
},
{
frequency: {
notifyWhen: 'onActionGroupChange',
summary: false,
throttle: null,
},
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {

View file

@ -58,6 +58,13 @@ export function populateAlertActions({
id: aId.id,
group: groupId,
params: {},
frequency: !isLegacy
? {
notifyWhen: 'onActionGroupChange',
throttle: null,
summary: false,
}
: undefined,
};
const recoveredAction: RuleAction = {
@ -66,6 +73,13 @@ export function populateAlertActions({
params: {
message: translations.defaultRecoveryMessage,
},
frequency: !isLegacy
? {
notifyWhen: 'onActionGroupChange',
throttle: null,
summary: false,
}
: undefined,
};
switch (aId.actionTypeId) {

View file

@ -72,3 +72,55 @@ export const SyntheticsMonitorStatusTranslations = {
defaultMessage: 'Alert when a monitor is down.',
}),
};
export const TlsTranslations = {
defaultActionMessage: i18n.translate('xpack.synthetics.rules.tls.defaultActionMessage', {
defaultMessage: `Detected TLS certificate {commonName} is {status} - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- Common name: {commonName}\n- Issuer: {issuer}\n- Monitor: {monitorName} \n- Monitor URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationName}`,
values: {
commonName: '{{context.commonName}}',
issuer: '{{context.issuer}}',
summary: '{{context.summary}}',
status: '{{context.status}}',
monitorName: '{{context.monitorName}}',
monitorUrl: '{{{context.monitorUrl}}}',
monitorType: '{{context.monitorType}}',
locationName: '{{context.locationName}}',
},
}),
defaultRecoveryMessage: i18n.translate('xpack.synthetics.rules.tls.defaultRecoveryMessage', {
defaultMessage: `Alert for TLS certificate {commonName} from issuer {issuer} has recovered - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- Common name: {commonName}\n- Issuer: {issuer}\n- Monitor: {monitorName} \n- Monitor URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationName}`,
values: {
commonName: '{{context.commonName}}',
issuer: '{{context.issuer}}',
summary: '{{context.summary}}',
monitorName: '{{context.monitorName}}',
monitorUrl: '{{{context.monitorUrl}}}',
monitorType: '{{context.monitorType}}',
locationName: '{{context.locationName}}',
},
}),
name: i18n.translate('xpack.synthetics.rules.tls.clientName', {
defaultMessage: 'Synthetics TLS',
}),
description: i18n.translate('xpack.synthetics.rules.tls.description', {
defaultMessage: 'Alert when the TLS certificate of a Synthetics monitor is about to expire.',
}),
defaultSubjectMessage: i18n.translate(
'xpack.synthetics.alerts.syntheticsMonitorTLS.defaultSubjectMessage',
{
defaultMessage: 'Alert triggered for certificate {commonName} - Elastic Synthetics',
values: {
commonName: '{{context.commonName}}',
},
}
),
defaultRecoverySubjectMessage: i18n.translate(
'xpack.synthetics.alerts.syntheticsMonitorTLS.defaultRecoverySubjectMessage',
{
defaultMessage: 'Alert has resolved for certificate {commonName} - Elastic Synthetics',
values: {
commonName: '{{context.commonName}}',
},
}
),
};

View file

@ -21,6 +21,7 @@ export const GetCertsParamsType = t.intersection([
direction: t.string,
size: t.number,
filters: t.unknown,
monitorIds: t.array(t.string),
}),
]);
@ -44,6 +45,10 @@ export const CertType = t.intersection([
common_name: t.string,
issuer: t.string,
sha1: t.string,
monitorName: t.string,
monitorType: t.string,
monitorUrl: t.string,
locationName: t.string,
}),
]);

View file

@ -94,6 +94,9 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
status: {
enabled: !(originalMonitorConfiguration?.alert?.status?.enabled as boolean),
},
tls: {
enabled: originalMonitorConfiguration?.alert?.tls?.enabled as boolean,
},
},
enabled: !originalMonitorConfiguration?.enabled,
});

View file

@ -0,0 +1,84 @@
/*
* 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 { EuiExpression, EuiFlexItem, EuiFlexGroup, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { ValueExpression } from '@kbn/triggers-actions-ui-plugin/public';
import { i18n } from '@kbn/i18n';
interface Props {
ageThreshold: number;
expirationThreshold: number;
setAgeThreshold: (value: number) => void;
setExpirationThreshold: (value: number) => void;
}
export const AlertTlsComponent: React.FC<Props> = ({
ageThreshold,
expirationThreshold,
setAgeThreshold,
setExpirationThreshold,
}) => (
<>
<EuiSpacer size="m" />
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
<EuiExpression
aria-label={TlsTranslations.criteriaAriaLabel}
color="success"
description={TlsTranslations.criteriaDescription}
value={TlsTranslations.criteriaValue}
/>
</EuiFlexItem>
<EuiFlexItem data-test-subj="tlsExpirationThreshold">
<ValueExpression
value={expirationThreshold}
onChangeSelectedValue={(val) => {
setExpirationThreshold(val);
}}
description={TlsTranslations.expirationDescription}
errors={[]}
/>
</EuiFlexItem>
<EuiFlexItem data-test-subj="tlsAgeExpirationThreshold">
<ValueExpression
value={ageThreshold}
onChangeSelectedValue={(val) => {
setAgeThreshold(val);
}}
description={TlsTranslations.ageDescription}
errors={[]}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
</>
);
export const TlsTranslations = {
criteriaAriaLabel: i18n.translate('xpack.synthetics.rules.tls.criteriaExpression.ariaLabel', {
defaultMessage:
'An expression displaying the criteria for the monitors that are being watched by this alert',
}),
criteriaDescription: i18n.translate(
'xpack.synthetics.alerts.tls.criteriaExpression.description',
{
defaultMessage: 'when',
description:
'The context of this `when` is in the conditional sense, like "when there are three cookies, eat them all".',
}
),
criteriaValue: i18n.translate('xpack.synthetics.tls.criteriaExpression.value', {
defaultMessage: 'matching monitor',
}),
expirationDescription: i18n.translate('xpack.synthetics.tls.expirationExpression.description', {
defaultMessage: 'has a certificate expiring within days: ',
}),
ageDescription: i18n.translate('xpack.synthetics.tls.ageExpression.description', {
defaultMessage: 'or older than days: ',
}),
};

View file

@ -18,9 +18,15 @@ export const ToggleFlyoutTranslations = {
toggleMonitorStatusAriaLabel: i18n.translate('xpack.synthetics.toggleAlertFlyout.ariaLabel', {
defaultMessage: 'Open add rule flyout',
}),
toggleTlsAriaLabel: i18n.translate('xpack.synthetics.toggleAlertFlyout.tls.ariaLabel', {
defaultMessage: 'Open add tls rule flyout',
}),
toggleMonitorStatusContent: i18n.translate('xpack.synthetics.toggleAlertButton.content', {
defaultMessage: 'Monitor status rule',
}),
toggleTlsContent: i18n.translate('xpack.synthetics.toggleTlsAlertButton.label.content', {
defaultMessage: 'TLS certificate rule',
}),
navigateToAlertingUIAriaLabel: i18n.translate('xpack.synthetics.app.navigateToAlertingUi', {
defaultMessage: 'Leave Synthetics and go to Alerting Management page',
}),

View file

@ -6,18 +6,20 @@
*/
import { useFetcher } from '@kbn/observability-shared-plugin/public';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect, useMemo, useState } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { setAlertFlyoutVisible } from '../../../state';
import { SYNTHETICS_TLS_RULE } from '../../../../../../common/constants/synthetics_alerts';
import { selectAlertFlyoutVisibility, setAlertFlyoutVisible } from '../../../state';
import { enableDefaultAlertingAPI } from '../../../state/alert_rules/api';
import { ClientPluginsStart } from '../../../../../plugin';
export const useSyntheticsAlert = (isOpen: boolean) => {
const dispatch = useDispatch();
const [alert, setAlert] = useState<Rule | null>(null);
const [defaultRules, setAlert] = useState<{ statusRule: Rule; tlsRule: Rule } | null>(null);
const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
const { data, loading } = useFetcher(() => {
if (isOpen) {
@ -34,15 +36,16 @@ export const useSyntheticsAlert = (isOpen: boolean) => {
const { triggersActionsUi } = useKibana<ClientPluginsStart>().services;
const EditAlertFlyout = useMemo(() => {
if (!alert) {
if (!defaultRules) {
return null;
}
return triggersActionsUi.getEditRuleFlyout({
onClose: () => dispatch(setAlertFlyoutVisible(false)),
onClose: () => dispatch(setAlertFlyoutVisible(null)),
hideInterval: true,
initialRule: alert,
initialRule:
alertFlyoutVisible === SYNTHETICS_TLS_RULE ? defaultRules.tlsRule : defaultRules.statusRule,
});
}, [alert, dispatch, triggersActionsUi]);
}, [defaultRules, dispatch, triggersActionsUi, alertFlyoutVisible]);
return useMemo(() => ({ loading, EditAlertFlyout }), [EditAlertFlyout, loading]);
};

View file

@ -0,0 +1,102 @@
/*
* 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 React, { useEffect, useState } from 'react';
import { EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { QueryStringInput } from '@kbn/unified-search-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useFetcher } from '@kbn/observability-shared-plugin/public';
import { SYNTHETICS_INDEX_PATTERN } from '../../../../../common/constants';
import { ClientPluginsStart } from '../../../../plugin';
interface Props {
query: string;
onChange: (query: string) => void;
}
export const isValidKuery = (query: string) => {
if (query === '') {
return true;
}
const listOfOperators = [':', '>=', '=>', '>', '<'];
for (let i = 0; i < listOfOperators.length; i++) {
const operator = listOfOperators[i];
const qParts = query.trim().split(operator);
if (query.includes(operator) && qParts.length > 1 && qParts[1]) {
return true;
}
}
return false;
};
export const AlertQueryBar = ({ query = '', onChange }: Props) => {
const { services } = useKibana<ClientPluginsStart>();
const {
appName,
notifications,
http,
docLinks,
uiSettings,
data,
dataViews,
unifiedSearch,
storage,
usageCollection,
} = services;
const [inputVal, setInputVal] = useState<string>(query);
const { data: dataView } = useFetcher(async () => {
return await dataViews.create({ title: SYNTHETICS_INDEX_PATTERN });
}, []);
useEffect(() => {
onChange(query);
setInputVal(query);
}, [onChange, query]);
return (
<EuiFlexItem grow={1} style={{ flexBasis: 485 }}>
<QueryStringInput
indexPatterns={dataView ? [dataView] : []}
iconType="search"
isClearable={true}
onChange={(queryN) => {
setInputVal(queryN?.query as string);
if (isValidKuery(queryN?.query as string)) {
// we want to submit when user clears or paste a complete kuery
onChange(queryN.query as string);
}
}}
onSubmit={(queryN) => {
if (queryN) onChange(queryN.query as string);
}}
query={{ query: inputVal, language: 'kuery' }}
dataTestSubj="xpack.synthetics.alerts.monitorStatus.filterBar"
autoSubmit={true}
disableLanguageSwitcher={true}
isInvalid={!!(inputVal && !query)}
placeholder={i18n.translate('xpack.synthetics.alerts.searchPlaceholder.kql', {
defaultMessage: 'Filter using kql syntax',
})}
appName={appName}
deps={{
unifiedSearch,
data,
dataViews,
storage,
notifications,
http,
docLinks,
uiSettings,
usageCollection,
}}
/>
</EuiFlexItem>
);
};

View file

@ -0,0 +1,47 @@
/*
* 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 { useDispatch, useSelector } from 'react-redux';
import React, { useEffect } from 'react';
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { AlertTlsComponent } from './alert_tls';
import { getDynamicSettings } from '../../state/settings/api';
import { selectDynamicSettings } from '../../state/settings';
import { TLSParams } from '../../../../../common/runtime_types/alerts/tls';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../common/constants';
export const TLSRuleComponent: React.FC<{
ruleParams: RuleTypeParamsExpressionProps<TLSParams>['ruleParams'];
setRuleParams: RuleTypeParamsExpressionProps<TLSParams>['setRuleParams'];
}> = ({ ruleParams, setRuleParams }) => {
const dispatch = useDispatch();
const { settings } = useSelector(selectDynamicSettings);
useEffect(() => {
if (typeof settings === 'undefined') {
dispatch(getDynamicSettings());
}
}, [dispatch, settings]);
return (
<AlertTlsComponent
ageThreshold={
ruleParams.certAgeThreshold ??
settings?.certAgeThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
}
expirationThreshold={
ruleParams.certExpirationThreshold ??
settings?.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
}
setAgeThreshold={(value) => setRuleParams('certAgeThreshold', Number(value))}
setExpirationThreshold={(value) => setRuleParams('certExpirationThreshold', Number(value))}
/>
);
};

View file

@ -19,6 +19,10 @@ import {
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
SYNTHETICS_STATUS_RULE,
SYNTHETICS_TLS_RULE,
} from '../../../../../common/constants/synthetics_alerts';
import { ManageRulesLink } from '../common/links/manage_rules_link';
import { ClientPluginsStart } from '../../../../plugin';
import { ToggleFlyoutTranslations } from './hooks/translations';
@ -48,7 +52,29 @@ export const ToggleAlertFlyoutButton = () => {
</EuiFlexGroup>
),
onClick: () => {
dispatch(setAlertFlyoutVisible(true));
dispatch(setAlertFlyoutVisible(SYNTHETICS_STATUS_RULE));
setIsOpen(false);
},
toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
disabled: !hasUptimeWrite || loading,
icon: 'bell',
};
const tlsAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
'data-test-subj': 'xpack.synthetics.toggleAlertFlyout.tls',
name: (
<EuiFlexGroup alignItems="center">
<EuiFlexItem>{ToggleFlyoutTranslations.toggleTlsContent}</EuiFlexItem>
{loading && (
<EuiFlexItem grow={false}>
<EuiLoadingSpinner />
</EuiFlexItem>
)}
</EuiFlexGroup>
),
onClick: () => {
dispatch(setAlertFlyoutVisible(SYNTHETICS_TLS_RULE));
setIsOpen(false);
},
toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
@ -66,7 +92,7 @@ export const ToggleAlertFlyoutButton = () => {
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
items: [monitorStatusAlertContextMenuItem, managementContextItem],
items: [monitorStatusAlertContextMenuItem, tlsAlertContextMenuItem, managementContextItem],
},
];

View file

@ -21,6 +21,10 @@ interface Page {
}
export type CertFields =
| 'monitorName'
| 'locationName'
| 'monitorType'
| 'monitorUrl'
| 'sha256'
| 'sha1'
| 'issuer'

View file

@ -500,6 +500,31 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
// isDisabled: readOnly,
}),
},
[AlertConfigKey.TLS_ENABLED]: {
fieldKey: AlertConfigKey.TLS_ENABLED,
component: Switch,
label: i18n.translate('xpack.synthetics.monitorConfig.enabledAlerting.tls.label', {
defaultMessage: 'Enable TLS alerts',
}),
controlled: true,
props: ({ isEdit, setValue, field }): EuiSwitchProps => ({
id: 'syntheticsMonitorConfigIsTlsAlertEnabled',
label: isEdit
? i18n.translate('xpack.synthetics.monitorConfig.edit.alertTlsEnabled.label', {
defaultMessage: 'Disabling will stop tls alerting on this monitor.',
})
: i18n.translate('xpack.synthetics.monitorConfig.create.alertTlsEnabled.label', {
defaultMessage: 'Enable tls alerts on this monitor.',
}),
checked: field?.value || false,
onChange: (event) => {
setValue(AlertConfigKey.TLS_ENABLED, !!event.target.checked);
},
'data-test-subj': 'syntheticsAlertStatusSwitch',
// alert config is an allowed field for read only
// isDisabled: readOnly,
}),
},
[ConfigKey.TAGS]: {
fieldKey: ConfigKey.TAGS,
component: FormattedComboBox,

View file

@ -199,6 +199,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
FIELD(readOnly)[ConfigKey.TIMEOUT],
FIELD(readOnly)[ConfigKey.ENABLED],
FIELD(readOnly)[AlertConfigKey.STATUS_ENABLED],
FIELD(readOnly)[AlertConfigKey.TLS_ENABLED],
],
advanced: [
DEFAULT_DATA_OPTIONS(readOnly),
@ -218,6 +219,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
FIELD(readOnly)[ConfigKey.TIMEOUT],
FIELD(readOnly)[ConfigKey.ENABLED],
FIELD(readOnly)[AlertConfigKey.STATUS_ENABLED],
FIELD(readOnly)[AlertConfigKey.TLS_ENABLED],
],
advanced: [
DEFAULT_DATA_OPTIONS(readOnly),
@ -285,6 +287,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
FIELD(readOnly)[ConfigKey.TIMEOUT],
FIELD(readOnly)[ConfigKey.ENABLED],
FIELD(readOnly)[AlertConfigKey.STATUS_ENABLED],
FIELD(readOnly)[AlertConfigKey.TLS_ENABLED],
],
advanced: [DEFAULT_DATA_OPTIONS(readOnly), ICMP_ADVANCED(readOnly).requestConfig],
},

View file

@ -45,6 +45,7 @@ export type FormConfig = MonitorFields & {
['schedule.number']: string;
['source.inline']: string;
[AlertConfigKey.STATUS_ENABLED]: boolean;
[AlertConfigKey.TLS_ENABLED]: boolean;
[ConfigKey.LOCATIONS]: FormLocation[];
/* Dot notation keys must have a type configuration both for their flattened and nested
@ -127,6 +128,7 @@ export interface FieldMap {
[ConfigKey.SCREENSHOTS]: FieldMeta<ConfigKey.SCREENSHOTS>;
[ConfigKey.ENABLED]: FieldMeta<ConfigKey.ENABLED>;
[AlertConfigKey.STATUS_ENABLED]: FieldMeta<AlertConfigKey.STATUS_ENABLED>;
[AlertConfigKey.TLS_ENABLED]: FieldMeta<AlertConfigKey.TLS_ENABLED>;
[ConfigKey.NAMESPACE]: FieldMeta<ConfigKey.NAMESPACE>;
[ConfigKey.TIMEOUT]: FieldMeta<ConfigKey.TIMEOUT>;
[ConfigKey.MAX_REDIRECTS]: FieldMeta<ConfigKey.MAX_REDIRECTS>;

View file

@ -7,6 +7,7 @@
import { CoreStart } from '@kbn/core/public';
import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public';
import { initTlsAlertType } from './tls';
import { ClientPluginsStart } from '../../../../plugin';
import { initMonitorStatusAlertType } from './monitor_status';
@ -15,4 +16,7 @@ export type AlertTypeInitializer<TAlertTypeModel = ObservabilityRuleTypeModel> =
plugins: ClientPluginsStart;
}) => TAlertTypeModel;
export const syntheticsAlertTypeInitializers: AlertTypeInitializer[] = [initMonitorStatusAlertType];
export const syntheticsAlertTypeInitializers: AlertTypeInitializer[] = [
initMonitorStatusAlertType,
initTlsAlertType,
];

View file

@ -0,0 +1,36 @@
/*
* 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 React from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import { CoreStart } from '@kbn/core/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { TLSRuleComponent } from '../../../components/alerts/tls_rule_ui';
import { ClientPluginsStart } from '../../../../../plugin';
import { TLSParams } from '../../../../../../common/runtime_types/alerts/tls';
import { kibanaService } from '../../../../../utils/kibana_service';
import { store } from '../../../state';
interface Props {
core: CoreStart;
plugins: ClientPluginsStart;
ruleParams: RuleTypeParamsExpressionProps<TLSParams>['ruleParams'];
setRuleParams: RuleTypeParamsExpressionProps<TLSParams>['setRuleParams'];
}
// eslint-disable-next-line import/no-default-export
export default function TLSAlert({ core, plugins, ruleParams, setRuleParams }: Props) {
kibanaService.core = core;
return (
<ReduxProvider store={store}>
<KibanaContextProvider services={{ ...core, ...plugins }}>
<TLSRuleComponent ruleParams={ruleParams} setRuleParams={setRuleParams} />
</KibanaContextProvider>
</ReduxProvider>
);
}

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { PathReporter } from 'io-ts/lib/PathReporter';
import { isRight } from 'fp-ts/lib/Either';
import { ValidationResult } from '@kbn/triggers-actions-ui-plugin/public';
import { TLSParamsType } from '../../../../../../common/runtime_types/alerts/tls';
export function validateTLSAlertParams(ruleParams: any): ValidationResult {
const errors: Record<string, any> = {};
const decoded = TLSParamsType.decode(ruleParams);
if (!isRight(decoded)) {
return {
errors: {
typeCheckFailure: 'Provided parameters do not conform to the expected type.',
typeCheckParsingMessage: PathReporter.report(decoded),
},
};
}
return { errors };
}

View file

@ -0,0 +1,57 @@
/*
* 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 React from 'react';
import { ALERT_REASON } from '@kbn/rule-data-utils';
import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public';
import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { ValidationResult } from '@kbn/triggers-actions-ui-plugin/public';
import { TlsTranslations } from '../../../../../common/rules/synthetics/translations';
import { CERTIFICATES_ROUTE } from '../../../../../common/constants/ui';
import { SYNTHETICS_ALERT_RULE_TYPES } from '../../../../../common/constants/synthetics_alerts';
import type { TLSParams } from '../../../../../common/runtime_types/alerts/tls';
import { AlertTypeInitializer } from '.';
let validateFunc: (ruleParams: any) => ValidationResult;
const { defaultActionMessage, defaultRecoveryMessage, description } = TlsTranslations;
const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert'));
export const initTlsAlertType: AlertTypeInitializer = ({
core,
plugins,
}): ObservabilityRuleTypeModel => ({
id: SYNTHETICS_ALERT_RULE_TYPES.TLS,
iconClass: 'uptimeApp',
documentationUrl(docLinks) {
return `${docLinks.links.observability.tlsCertificate}`;
},
ruleParamsExpression: (params: RuleTypeParamsExpressionProps<TLSParams>) => (
<TLSAlert
core={core}
plugins={plugins}
ruleParams={params.ruleParams}
setRuleParams={params.setRuleParams}
/>
),
description,
validate: (ruleParams: any) => {
if (!validateFunc) {
(async function loadValidate() {
const { validateTLSAlertParams } = await import('./lazy_wrapper/validate_tls_alert');
validateFunc = validateTLSAlertParams;
})();
}
return validateFunc ? validateFunc(ruleParams) : ({} as ValidationResult);
},
defaultActionMessage,
defaultRecoveryMessage,
requiresAppContext: false,
format: ({ fields }) => ({
reason: fields[ALERT_REASON] || '',
link: `/app/synthetics${CERTIFICATES_ROUTE}`,
}),
});

View file

@ -8,9 +8,10 @@
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { createAsyncAction } from '../utils/actions';
export const enableDefaultAlertingAction = createAsyncAction<void, Rule>(
'enableDefaultAlertingAction'
);
export const enableDefaultAlertingAction = createAsyncAction<
void,
{ statusRule: Rule; tlsRule: Rule }
>('enableDefaultAlertingAction');
export const updateDefaultAlertingAction = createAsyncAction<void, Rule>(
'updateDefaultAlertingAction'

View file

@ -9,7 +9,7 @@ import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { SYNTHETICS_API_URLS } from '../../../../../common/constants';
import { apiService } from '../../../../utils/api_service';
export async function enableDefaultAlertingAPI(): Promise<Rule> {
export async function enableDefaultAlertingAPI(): Promise<{ statusRule: Rule; tlsRule: Rule }> {
return apiService.post(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING);
}

View file

@ -6,15 +6,19 @@
*/
import { createAction } from '@reduxjs/toolkit';
import {
SYNTHETICS_STATUS_RULE,
SYNTHETICS_TLS_RULE,
} from '../../../../../common/constants/synthetics_alerts';
export interface PopoverState {
id: string;
open: boolean;
}
export const setAlertFlyoutVisible = createAction<boolean | undefined>('[UI] TOGGLE ALERT FLYOUT');
export const setAlertFlyoutType = createAction<string>('[UI] SET ALERT FLYOUT TYPE');
export const setAlertFlyoutVisible = createAction<
typeof SYNTHETICS_STATUS_RULE | typeof SYNTHETICS_TLS_RULE | null
>('[UI] TOGGLE ALERT FLYOUT');
export const setBasePath = createAction<string>('[UI] SET BASE PATH');

View file

@ -7,13 +7,16 @@
import { createReducer } from '@reduxjs/toolkit';
import {
SYNTHETICS_STATUS_RULE,
SYNTHETICS_TLS_RULE,
} from '../../../../../common/constants/synthetics_alerts';
import { CLIENT_DEFAULTS_SYNTHETICS } from '../../../../../common/constants/synthetics/client_defaults';
import {
PopoverState,
toggleIntegrationsPopover,
setBasePath,
setEsKueryString,
setAlertFlyoutType,
setAlertFlyoutVisible,
setSearchTextAction,
setSelectedMonitorId,
@ -23,8 +26,7 @@ import {
const { AUTOREFRESH_INTERVAL_SECONDS, AUTOREFRESH_IS_PAUSED } = CLIENT_DEFAULTS_SYNTHETICS;
export interface UiState {
alertFlyoutVisible: boolean;
alertFlyoutType?: string;
alertFlyoutVisible: typeof SYNTHETICS_TLS_RULE | typeof SYNTHETICS_STATUS_RULE | null;
basePath: string;
esKuery: string;
searchText: string;
@ -35,7 +37,7 @@ export interface UiState {
}
const initialState: UiState = {
alertFlyoutVisible: false,
alertFlyoutVisible: null,
basePath: '',
esKuery: '',
searchText: '',
@ -51,10 +53,7 @@ export const uiReducer = createReducer(initialState, (builder) => {
state.integrationsPopoverOpen = action.payload;
})
.addCase(setAlertFlyoutVisible, (state, action) => {
state.alertFlyoutVisible = action.payload ?? !state.alertFlyoutVisible;
})
.addCase(setAlertFlyoutType, (state, action) => {
state.alertFlyoutType = action.payload;
state.alertFlyoutVisible = action.payload;
})
.addCase(setBasePath, (state, action) => {
state.basePath = action.payload;

View file

@ -9,29 +9,12 @@ import { createSelector } from 'reselect';
import type { SyntheticsAppState } from '../root_reducer';
const uiStateSelector = (appState: SyntheticsAppState) => appState.ui;
export const selectBasePath = createSelector(uiStateSelector, ({ basePath }) => basePath);
export const selectIsIntegrationsPopupOpen = createSelector(
uiStateSelector,
({ integrationsPopoverOpen }) => integrationsPopoverOpen
);
export const selectAlertFlyoutVisibility = createSelector(
uiStateSelector,
({ alertFlyoutVisible }) => alertFlyoutVisible
);
export const selectAlertFlyoutType = createSelector(
uiStateSelector,
({ alertFlyoutType }) => alertFlyoutType
);
export const selectEsKuery = createSelector(uiStateSelector, ({ esKuery }) => esKuery);
export const selectSearchText = createSelector(uiStateSelector, ({ searchText }) => searchText);
export const selectMonitorId = createSelector(uiStateSelector, ({ monitorId }) => monitorId);
export const selectRefreshPaused = createSelector(
uiStateSelector,
({ refreshPaused }) => refreshPaused

View file

@ -23,7 +23,7 @@ import {
*/
export const mockState: SyntheticsAppState = {
ui: {
alertFlyoutVisible: false,
alertFlyoutVisible: null,
basePath: 'yyz',
esKuery: '',
integrationsPopoverOpen: null,

View file

@ -21,6 +21,10 @@ interface Page {
}
export type CertFields =
| 'monitorName'
| 'locationName'
| 'monitorType'
| 'monitorUrl'
| 'sha256'
| 'sha1'
| 'issuer'

View file

@ -0,0 +1,70 @@
/*
* 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 moment from 'moment/moment';
import { tlsTranslations } from '../translations';
import { Cert } from '../../../common/runtime_types';
interface TLSContent {
summary: string;
status?: string;
}
const getValidBefore = ({ not_before: date }: Cert): TLSContent => {
if (!date) return { summary: 'Error, missing `certificate_not_valid_before` date.' };
const relativeDate = moment().diff(date, 'days');
const formattedDate = moment(date).format('MMM D, YYYY z');
return relativeDate >= 0
? {
summary: tlsTranslations.validBeforeExpiredString(formattedDate, relativeDate),
status: tlsTranslations.agingLabel,
}
: {
summary: tlsTranslations.validBeforeExpiringString(formattedDate, Math.abs(relativeDate)),
status: tlsTranslations.invalidLabel,
};
};
const getValidAfter = ({ not_after: date }: Cert): TLSContent => {
if (!date) return { summary: 'Error, missing `certificate_not_valid_after` date.' };
const relativeDate = moment().diff(date, 'days');
const formattedDate = moment(date).format('MMM D, YYYY z');
return relativeDate >= 0
? {
summary: tlsTranslations.validAfterExpiredString(formattedDate, relativeDate),
status: tlsTranslations.expiredLabel,
}
: {
summary: tlsTranslations.validAfterExpiringString(formattedDate, Math.abs(relativeDate)),
status: tlsTranslations.expiringLabel,
};
};
const mapCertsToSummaryString = (
cert: Cert,
certLimitMessage: (cert: Cert) => TLSContent
): TLSContent => certLimitMessage(cert);
export const getCertSummary = (cert: Cert, expirationThreshold: number, ageThreshold: number) => {
const isExpiring = new Date(cert.not_after ?? '').valueOf() < expirationThreshold;
const isAging = new Date(cert.not_before ?? '').valueOf() < ageThreshold;
let content: TLSContent | null = null;
if (isExpiring) {
content = mapCertsToSummaryString(cert, getValidAfter);
} else if (isAging) {
content = mapCertsToSummaryString(cert, getValidBefore);
}
const { summary = '', status = '' } = content || {};
return {
summary,
status,
commonName: cert.common_name ?? '',
issuer: cert.issuer ?? '',
monitorName: cert.monitorName,
monitorType: cert.monitorType,
locationName: cert.locationName,
monitorUrl: cert.monitorUrl,
};
};

View file

@ -0,0 +1,161 @@
/*
* 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 { ActionGroupIdsOf } from '@kbn/alerting-plugin/common';
import { createLifecycleRuleTypeFactory, IRuleDataClient } from '@kbn/rule-registry-plugin/server';
import { asyncForEach } from '@kbn/std';
import { ALERT_REASON, ALERT_UUID } from '@kbn/rule-data-utils';
import {
alertsLocatorID,
AlertsLocatorParams,
getAlertUrl,
} from '@kbn/observability-plugin/common';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { schema } from '@kbn/config-schema';
import { TlsTranslations } from '../../../common/rules/synthetics/translations';
import {
CERT_COMMON_NAME,
CERT_HASH_SHA256,
CERT_ISSUER_NAME,
CERT_VALID_NOT_AFTER,
CERT_VALID_NOT_BEFORE,
} from '../../../common/field_names';
import { getCertSummary } from './message_utils';
import { SyntheticsCommonState } from '../../../common/runtime_types/alert_rules/common';
import { UptimeCorePluginsSetup, UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { TLSRuleExecutor } from './tls_rule_executor';
import {
SYNTHETICS_ALERT_RULE_TYPES,
TLS_CERTIFICATE,
} from '../../../common/constants/synthetics_alerts';
import { updateState } from '../common';
import { getActionVariables } from '../action_variables';
import { ALERT_DETAILS_URL } from '../../legacy_uptime/lib/alerts/action_variables';
import { UMServerLibs } from '../../legacy_uptime/uptime_server';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import {
generateAlertMessage,
setRecoveredAlertsContext,
UptimeRuleTypeAlertDefinition,
} from '../../legacy_uptime/lib/alerts/common';
export type ActionGroupIds = ActionGroupIdsOf<typeof TLS_CERTIFICATE>;
export const registerSyntheticsTLSCheckRule = (
server: UptimeServerSetup,
libs: UMServerLibs,
plugins: UptimeCorePluginsSetup,
syntheticsMonitorClient: SyntheticsMonitorClient,
ruleDataClient: IRuleDataClient
) => {
const createLifecycleRuleType = createLifecycleRuleTypeFactory({
ruleDataClient,
logger: server.logger,
});
return createLifecycleRuleType({
id: SYNTHETICS_ALERT_RULE_TYPES.TLS,
producer: 'uptime',
name: TLS_CERTIFICATE.name,
validate: {
params: schema.object({
search: schema.maybe(schema.string()),
certExpirationThreshold: schema.maybe(schema.number()),
certAgeThreshold: schema.maybe(schema.number()),
}),
},
defaultActionGroupId: TLS_CERTIFICATE.id,
actionGroups: [TLS_CERTIFICATE],
actionVariables: getActionVariables({ plugins }),
isExportable: true,
minimumLicenseRequired: 'basic',
doesSetRecoveryContext: true,
async executor({ state, params, services, spaceId, previousStartedAt, startedAt }) {
const ruleState = state as SyntheticsCommonState;
const { basePath, share } = server;
const alertsLocator: LocatorPublic<AlertsLocatorParams> | undefined =
share.url.locators.get(alertsLocatorID);
const {
alertFactory,
getAlertUuid,
savedObjectsClient,
scopedClusterClient,
alertWithLifecycle,
getAlertStartedDate,
} = services;
const tlsRule = new TLSRuleExecutor(
previousStartedAt,
params,
savedObjectsClient,
scopedClusterClient.asCurrentUser,
server,
syntheticsMonitorClient
);
const { foundCerts, certs, absoluteExpirationThreshold, absoluteAgeThreshold } =
await tlsRule.getExpiredCertificates();
if (foundCerts) {
await asyncForEach(certs, async (cert) => {
const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold);
if (!summary.summary || !summary.status) {
return;
}
const alertId = `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`;
const alertUuid = getAlertUuid(alertId);
const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString();
const alertInstance = alertWithLifecycle({
id: alertId,
fields: {
[CERT_COMMON_NAME]: cert.common_name,
[CERT_ISSUER_NAME]: cert.issuer,
[CERT_VALID_NOT_AFTER]: cert.not_after,
[CERT_VALID_NOT_BEFORE]: cert.not_before,
[CERT_HASH_SHA256]: cert.sha256,
[ALERT_UUID]: alertUuid,
[ALERT_REASON]: generateAlertMessage(TlsTranslations.defaultActionMessage, summary),
},
});
alertInstance.replaceState({
...updateState(ruleState, foundCerts),
...summary,
});
alertInstance.scheduleActions(TLS_CERTIFICATE.id, {
[ALERT_DETAILS_URL]: await getAlertUrl(
alertUuid,
spaceId,
indexedStartedAt,
alertsLocator,
basePath.publicBaseUrl
),
...summary,
});
});
}
await setRecoveredAlertsContext({
alertFactory,
basePath,
defaultStartedAt: startedAt.toISOString(),
getAlertStartedDate,
getAlertUuid,
spaceId,
alertsLocator,
});
return { state: updateState(ruleState, foundCerts) };
},
alerts: UptimeRuleTypeAlertDefinition,
});
};

View file

@ -0,0 +1,82 @@
/*
* 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 moment from 'moment';
import { loggerMock } from '@kbn/logging-mocks';
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
import { TLSRuleExecutor } from './tls_rule_executor';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { mockEncryptedSO } from '../../synthetics_service/utils/mocks';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { SyntheticsService } from '../../synthetics_service/synthetics_service';
import * as monitorUtils from '../../saved_objects/synthetics_monitor/get_all_monitors';
import * as locationsUtils from '../../synthetics_service/get_all_locations';
import type { PublicLocation } from '../../../common/runtime_types';
describe('tlsRuleExecutor', () => {
const mockEsClient = elasticsearchClientMock.createElasticsearchClient();
const logger = loggerMock.create();
const soClient = savedObjectsClientMock.create();
jest.spyOn(locationsUtils, 'getAllLocations').mockResolvedValue({
// @ts-ignore
publicLocations: [
{
id: 'us_central_qa',
label: 'US Central QA',
},
{
id: 'us_central_dev',
label: 'US Central DEV',
},
] as unknown as PublicLocation,
privateLocations: [],
});
const serverMock: UptimeServerSetup = {
logger,
uptimeEsClient: mockEsClient,
authSavedObjectsClient: soClient,
config: {
service: {
username: 'dev',
password: '12345',
manifestUrl: 'http://localhost:8080/api/manifest',
},
},
spaces: {
spacesService: {
getSpaceId: jest.fn().mockReturnValue('test-space'),
},
},
encryptedSavedObjects: mockEncryptedSO(),
} as unknown as UptimeServerSetup;
const syntheticsService = new SyntheticsService(serverMock);
const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
it('should only query enabled monitors', async () => {
const spy = jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([]);
const tlsRule = new TLSRuleExecutor(
moment().toDate(),
{},
soClient,
mockEsClient,
serverMock,
monitorClient
);
const { certs } = await tlsRule.getExpiredCertificates();
expect(certs).toEqual([]);
expect(spy).toHaveBeenCalledWith({
filter: 'synthetics-monitor.attributes.alert.tls.enabled: true',
soClient,
});
});
});

View file

@ -0,0 +1,147 @@
/*
* 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,
SavedObjectsFindResult,
} from '@kbn/core-saved-objects-api-server';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import moment from 'moment';
import { getSyntheticsCerts } from '../../queries/get_certs';
import { TLSParams } from '../../../common/runtime_types/alerts/tls';
import { savedObjectsAdapter } from '../../legacy_uptime/lib/saved_objects';
import { DYNAMIC_SETTINGS_DEFAULTS, SYNTHETICS_INDEX_PATTERN } from '../../../common/constants';
import {
getAllMonitors,
processMonitors,
} from '../../saved_objects/synthetics_monitor/get_all_monitors';
import { UptimeEsClient } from '../../legacy_uptime/lib/lib';
import { CertResult, EncryptedSyntheticsMonitor } from '../../../common/runtime_types';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { monitorAttributes } from '../../../common/types/saved_objects';
import { AlertConfigKey } from '../../../common/constants/monitor_management';
import { formatFilterString } from '../../legacy_uptime/lib/alerts/status_check';
export class TLSRuleExecutor {
previousStartedAt: Date | null;
params: TLSParams;
esClient: UptimeEsClient;
soClient: SavedObjectsClientContract;
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
monitors: Array<SavedObjectsFindResult<EncryptedSyntheticsMonitor>> = [];
constructor(
previousStartedAt: Date | null,
p: TLSParams,
soClient: SavedObjectsClientContract,
scopedClient: ElasticsearchClient,
server: UptimeServerSetup,
syntheticsMonitorClient: SyntheticsMonitorClient
) {
this.previousStartedAt = previousStartedAt;
this.params = p;
this.soClient = soClient;
this.esClient = new UptimeEsClient(this.soClient, scopedClient, {
heartbeatIndices: SYNTHETICS_INDEX_PATTERN,
});
this.server = server;
this.syntheticsMonitorClient = syntheticsMonitorClient;
}
async getMonitors() {
this.monitors = await getAllMonitors({
soClient: this.soClient,
filter: `${monitorAttributes}.${AlertConfigKey.TLS_ENABLED}: true`,
});
const {
allIds,
enabledMonitorQueryIds,
listOfLocations,
monitorLocationMap,
projectMonitorsCount,
monitorQueryIdToConfigIdMap,
} = await processMonitors(
this.monitors,
this.server,
this.soClient,
this.syntheticsMonitorClient
);
return {
enabledMonitorQueryIds,
listOfLocations,
allIds,
monitorLocationMap,
projectMonitorsCount,
monitorQueryIdToConfigIdMap,
};
}
async getExpiredCertificates() {
const { enabledMonitorQueryIds } = await this.getMonitors();
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(this.soClient);
const expiryThreshold =
this.params.certExpirationThreshold ??
dynamicSettings?.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold;
const ageThreshold =
this.params.certAgeThreshold ??
dynamicSettings?.certAgeThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold;
const absoluteExpirationThreshold = moment().add(expiryThreshold, 'd').valueOf();
const absoluteAgeThreshold = moment().subtract(ageThreshold, 'd').valueOf();
if (enabledMonitorQueryIds.length === 0) {
return {
certs: [],
total: 0,
foundCerts: false,
expiryThreshold,
ageThreshold,
absoluteExpirationThreshold,
absoluteAgeThreshold,
};
}
let filters: QueryDslQueryContainer | undefined;
if (this.params.search) {
filters = await formatFilterString(this.esClient, undefined, this.params.search);
}
const { certs, total }: CertResult = await getSyntheticsCerts({
uptimeEsClient: this.esClient,
pageIndex: 0,
size: 1000,
notValidAfter: `now+${expiryThreshold}d`,
notValidBefore: `now-${ageThreshold}d`,
sortBy: 'common_name',
direction: 'desc',
filters,
monitorIds: enabledMonitorQueryIds,
});
const foundCerts = total > 0;
return {
foundCerts,
certs,
total,
expiryThreshold,
ageThreshold,
absoluteExpirationThreshold,
absoluteAgeThreshold,
};
}
}

View file

@ -0,0 +1,23 @@
/*
* 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 interface MonitorSummaryStatusRule {
reason: string;
status: string;
configId: string;
hostName: string;
monitorId: string;
checkedAt: string;
monitorUrl: string;
locationId: string;
monitorType: string;
monitorName: string;
locationName: string;
lastErrorMessage: string;
stateId: string | null;
monitorUrlLabel: string;
}

View file

@ -164,3 +164,91 @@ export const commonStateTranslations = [
),
},
];
export const tlsTranslations = {
actionVariables: [
{
name: 'count',
description: i18n.translate('xpack.synthetics.rules.tls.actionVariables.state.count', {
defaultMessage: 'The number of certs detected by the alert executor',
}),
},
{
name: 'expiringCount',
description: i18n.translate(
'xpack.synthetics.rules.tls.actionVariables.state.expiringCount',
{
defaultMessage: 'The number of expiring certs detected by the alert.',
}
),
},
{
name: 'expiringCommonNameAndDate',
description: i18n.translate(
'xpack.synthetics.rules.tls.actionVariables.state.expiringCommonNameAndDate',
{
defaultMessage: 'The common names and expiration date/time of the detected certs',
}
),
},
{
name: 'agingCount',
description: i18n.translate('xpack.synthetics.rules.tls.actionVariables.state.agingCount', {
defaultMessage: 'The number of detected certs that are becoming too old.',
}),
},
{
name: 'agingCommonNameAndDate',
description: i18n.translate(
'xpack.synthetics.rules.tls.actionVariables.state.agingCommonNameAndDate',
{
defaultMessage: 'The common names and expiration date/time of the detected certs.',
}
),
},
],
validAfterExpiredString: (date: string, relativeDate: number) =>
i18n.translate('xpack.synthetics.rules.tls.validAfterExpiredString', {
defaultMessage: `Expired on {date}, {relativeDate} days ago.`,
values: {
date,
relativeDate,
},
}),
validAfterExpiringString: (date: string, relativeDate: number) =>
i18n.translate('xpack.synthetics.rules.tls.validAfterExpiringString', {
defaultMessage: `Expires on {date} in {relativeDate} days.`,
values: {
date,
relativeDate,
},
}),
validBeforeExpiredString: (date: string, relativeDate: number) =>
i18n.translate('xpack.synthetics.rules.tls.validBeforeExpiredString', {
defaultMessage: 'valid since {date}, {relativeDate} days ago.',
values: {
date,
relativeDate,
},
}),
validBeforeExpiringString: (date: string, relativeDate: number) =>
i18n.translate('xpack.synthetics.rules.tls.validBeforeExpiringString', {
defaultMessage: 'invalid until {date}, {relativeDate} days from now.',
values: {
date,
relativeDate,
},
}),
expiredLabel: i18n.translate('xpack.synthetics.rules.tls.expiredLabel', {
defaultMessage: 'expired',
}),
expiringLabel: i18n.translate('xpack.synthetics.rules.tls.expiringLabel', {
defaultMessage: 'expiring',
}),
agingLabel: i18n.translate('xpack.synthetics.rules.tls.agingLabel', {
defaultMessage: 'becoming too old',
}),
invalidLabel: i18n.translate('xpack.synthetics.rules.tls.invalidLabel', {
defaultMessage: 'invalid',
}),
};

View file

@ -112,6 +112,10 @@ describe('getCerts', () => {
Object {
"common_name": "r2.shared.global.fastly.net",
"issuer": "GlobalSign CloudSSL CA - SHA256 - G3",
"locationName": undefined,
"monitorName": "Real World Test",
"monitorType": undefined,
"monitorUrl": "https://fullurl.com",
"monitors": Array [
Object {
"configId": undefined,
@ -137,6 +141,9 @@ describe('getCerts', () => {
"_source": Array [
"monitor.id",
"monitor.name",
"monitor.type",
"url.full",
"observer.geo.name",
"tls.server.x509.issuer.common_name",
"tls.server.x509.subject.common_name",
"tls.server.hash.sha1",

View file

@ -0,0 +1,88 @@
/*
* 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 { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { migrationMocks } from '@kbn/core/server/mocks';
import { ConfigKey } from '../../../../../../common/runtime_types';
import { browserUI } from './test_fixtures/8.7.0';
import { httpUI as httpUI850 } from './test_fixtures/8.5.0';
import { migration890 } from './8.9.0';
const context = migrationMocks.createContext();
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
describe('Monitor migrations v8.8.0 -> v8.9.0', () => {
beforeEach(() => {
jest.resetAllMocks();
encryptedSavedObjectsSetup.createMigration.mockImplementation(({ migration }) => migration);
});
describe('alerting config', () => {
it('sets alerting config when it is not defined', () => {
expect(httpUI850.attributes[ConfigKey.ALERT_CONFIG]).toBeUndefined();
const actual = migration890(encryptedSavedObjectsSetup)(httpUI850, context);
expect(actual.attributes[ConfigKey.ALERT_CONFIG]).toEqual({
status: {
enabled: true,
},
tls: {
enabled: true,
},
});
});
it('uses existing alerting config when it is defined', () => {
const testMonitor = {
...browserUI,
attributes: {
...browserUI.attributes,
[ConfigKey.ALERT_CONFIG]: {
status: {
enabled: false,
},
tls: {
enabled: true,
},
},
},
};
expect(testMonitor.attributes[ConfigKey.ALERT_CONFIG]).toBeTruthy();
const actual = migration890(encryptedSavedObjectsSetup)(testMonitor, context);
expect(actual.attributes[ConfigKey.ALERT_CONFIG]).toEqual({
status: {
enabled: false,
},
tls: {
enabled: true,
},
});
});
it('uses existing alerting config when it already exists', () => {
const testMonitor = {
...browserUI,
attributes: {
...browserUI.attributes,
[ConfigKey.ALERT_CONFIG]: {
status: {
enabled: false,
},
},
},
};
expect(testMonitor.attributes[ConfigKey.ALERT_CONFIG]).toBeTruthy();
const actual = migration890(encryptedSavedObjectsSetup)(testMonitor, context);
expect(actual.attributes[ConfigKey.ALERT_CONFIG]).toEqual({
status: {
enabled: false,
},
tls: {
enabled: true,
},
});
});
});
});

View file

@ -0,0 +1,52 @@
/*
* 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 { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { SavedObjectUnsanitizedDoc } from '@kbn/core/server';
import { ConfigKey, SyntheticsMonitorWithSecrets } from '../../../../../../common/runtime_types';
import { SYNTHETICS_MONITOR_ENCRYPTED_TYPE } from '../../synthetics_monitor';
export const migration890 = (encryptedSavedObjects: EncryptedSavedObjectsPluginSetup) => {
return encryptedSavedObjects.createMigration<
SyntheticsMonitorWithSecrets,
SyntheticsMonitorWithSecrets
>({
isMigrationNeededPredicate: function shouldBeMigrated(
doc
): doc is SavedObjectUnsanitizedDoc<SyntheticsMonitorWithSecrets> {
return true;
},
migration: (
doc: SavedObjectUnsanitizedDoc<SyntheticsMonitorWithSecrets>
): SavedObjectUnsanitizedDoc<SyntheticsMonitorWithSecrets> => {
let migrated = doc;
migrated = {
...migrated,
attributes: {
...migrated.attributes,
[ConfigKey.ALERT_CONFIG]: {
status: {
enabled: true,
},
tls: {
enabled: true,
},
...(migrated.attributes[ConfigKey.ALERT_CONFIG] ?? {}),
},
// when any action to change a project monitor configuration is taken
// outside the synthetics agent cli, we should set the config hash back
// to an empty string so that the project monitors configuration
// will be updated on next push
[ConfigKey.CONFIG_HASH]: '',
},
};
return migrated;
},
inputType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
migratedType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
});
};

View file

@ -5,10 +5,12 @@
* 2.0.
*/
import { migration890 } from './8.9.0';
import { migration860 } from './8.6.0';
import { migration880 } from './8.8.0';
export const monitorMigrations = {
'8.6.0': migration860,
'8.8.0': migration880,
'8.9.0': migration890,
};

View file

@ -60,6 +60,7 @@ export const getSyntheticsMonitorSavedObjectType = (
migrations: {
'8.6.0': monitorMigrations['8.6.0'](encryptedSavedObjects),
'8.8.0': monitorMigrations['8.8.0'](encryptedSavedObjects),
'8.9.0': monitorMigrations['8.9.0'](encryptedSavedObjects),
},
mappings: {
dynamic: false,
@ -167,6 +168,13 @@ export const getSyntheticsMonitorSavedObjectType = (
},
},
},
tls: {
properties: {
enabled: {
type: 'boolean',
},
},
},
},
},
throttling: {

View file

@ -0,0 +1,37 @@
/*
* 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 { CertResult, GetCertsParams, Ping } from '../../common/runtime_types';
import {
getCertsRequestBody,
processCertsResult,
} from '../../common/requests/get_certs_request_body';
import { UptimeEsClient } from '../legacy_uptime/lib/lib';
export const getSyntheticsCerts = async (
requestParams: GetCertsParams & { uptimeEsClient: UptimeEsClient }
): Promise<CertResult> => {
const result = await getCertsResults(requestParams);
return processCertsResult(result);
};
const getCertsResults = async (
requestParams: GetCertsParams & { uptimeEsClient: UptimeEsClient }
) => {
const { uptimeEsClient } = requestParams;
const searchBody = getCertsRequestBody(requestParams);
const request = { body: searchBody };
const { body: result } = await uptimeEsClient.search<Ping, typeof request>({
body: searchBody,
});
return result;
};

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { TLSAlertService } from './tls_alert_service';
import { StatusAlertService } from './status_alert_service';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
@ -16,7 +17,23 @@ export const enableDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => (
writeAccess: true,
handler: async ({ context, server, savedObjectsClient }): Promise<any> => {
const statusAlertService = new StatusAlertService(context, server, savedObjectsClient);
const tlsAlertService = new TLSAlertService(context, server, savedObjectsClient);
return await statusAlertService.createDefaultAlertIfNotExist();
const [statusRule, tlsRule] = await Promise.allSettled([
statusAlertService.createDefaultAlertIfNotExist(),
tlsAlertService.createDefaultAlertIfNotExist(),
]);
if (statusRule.status === 'rejected') {
throw statusRule.reason;
}
if (tlsRule.status === 'rejected') {
throw tlsRule.reason;
}
return {
statusRule: statusRule.status === 'fulfilled' ? statusRule.value : null,
tlsRule: tlsRule.status === 'fulfilled' ? tlsRule.value : null,
};
},
});

View file

@ -66,7 +66,6 @@ export class StatusAlertService {
consumer: 'uptime',
alertTypeId: SYNTHETICS_ALERT_RULE_TYPES.MONITOR_STATUS,
schedule: { interval: '1m' },
notifyWhen: 'onActionGroupChange',
tags: ['SYNTHETICS_DEFAULT_ALERT'],
name: `Synthetics internal alert`,
enabled: true,

View file

@ -0,0 +1,131 @@
/*
* 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 '@kbn/core-saved-objects-api-server';
import { FindActionResult } from '@kbn/actions-plugin/server';
import { TLSParams } from '../../../common/runtime_types/alerts/tls';
import { savedObjectsAdapter } from '../../legacy_uptime/lib/saved_objects';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { populateAlertActions } from '../../../common/rules/alert_actions';
import { TlsTranslations } from '../../../common/rules/synthetics/translations';
import { UptimeRequestHandlerContext } from '../../types';
import {
ACTION_GROUP_DEFINITIONS,
SYNTHETICS_ALERT_RULE_TYPES,
} from '../../../common/constants/synthetics_alerts';
// uuid based on the string 'uptime-tls-default-alert'
const TLS_DEFAULT_ALERT_ID = '7a532181-ff1d-4317-9367-7ca789133920';
export class TLSAlertService {
context: UptimeRequestHandlerContext;
soClient: SavedObjectsClientContract;
server: UptimeServerSetup;
constructor(
context: UptimeRequestHandlerContext,
server: UptimeServerSetup,
soClient: SavedObjectsClientContract
) {
this.context = context;
this.server = server;
this.soClient = soClient;
}
async getExistingAlert() {
const rulesClient = (await this.context.alerting)?.getRulesClient();
try {
const alert = await rulesClient.get({ id: TLS_DEFAULT_ALERT_ID });
return { ...alert, ruleTypeId: alert.alertTypeId };
} catch (e) {
return null;
}
}
async createDefaultAlertIfNotExist() {
const alert = await this.getExistingAlert();
if (alert) {
return alert;
}
const actions = await this.getAlertActions();
const rulesClient = (await this.context.alerting)?.getRulesClient();
const newAlert = await rulesClient.create<TLSParams>({
data: {
actions,
params: {},
consumer: 'uptime',
alertTypeId: SYNTHETICS_ALERT_RULE_TYPES.TLS,
schedule: { interval: '1m' },
tags: ['SYNTHETICS_TLS_DEFAULT_ALERT'],
name: `Synthetics internal TLS alert`,
enabled: true,
throttle: null,
},
options: {
id: TLS_DEFAULT_ALERT_ID,
},
});
return { ...newAlert, ruleTypeId: newAlert.alertTypeId };
}
async updateDefaultAlert() {
const rulesClient = (await this.context.alerting)?.getRulesClient();
const alert = await this.getExistingAlert();
if (alert) {
const actions = await this.getAlertActions();
const updatedAlert = await rulesClient.update({
id: alert.id,
data: {
actions,
name: alert.name,
tags: alert.tags,
schedule: alert.schedule,
params: alert.params,
notifyWhen: alert.notifyWhen,
},
});
return { ...updatedAlert, ruleTypeId: updatedAlert.alertTypeId };
}
return await this.createDefaultAlertIfNotExist();
}
async getAlertActions() {
const { actionConnectors, settings } = await this.getActionConnectors();
const defaultActions = (actionConnectors ?? []).filter((act) =>
settings?.defaultConnectors?.includes(act.id)
);
return populateAlertActions({
groupId: ACTION_GROUP_DEFINITIONS.TLS_CERTIFICATE.id,
defaultActions,
defaultEmail: settings?.defaultEmail!,
translations: {
defaultActionMessage: TlsTranslations.defaultActionMessage,
defaultRecoveryMessage: TlsTranslations.defaultRecoveryMessage,
defaultSubjectMessage: TlsTranslations.defaultSubjectMessage,
defaultRecoverySubjectMessage: TlsTranslations.defaultRecoverySubjectMessage,
},
});
}
async getActionConnectors() {
const actionsClient = (await this.context.actions)?.getActionsClient();
const settings = await savedObjectsAdapter.getUptimeDynamicSettings(this.soClient);
let actionConnectors: FindActionResult[] = [];
try {
actionConnectors = await actionsClient.getAll();
} catch (e) {
this.server.logger.error(e);
}
return { actionConnectors, settings };
}
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { TLSAlertService } from './tls_alert_service';
import { StatusAlertService } from './status_alert_service';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
@ -16,7 +17,11 @@ export const updateDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => (
writeAccess: true,
handler: async ({ context, server, savedObjectsClient }): Promise<any> => {
const statusAlertService = new StatusAlertService(context, server, savedObjectsClient);
const tlsAlertService = new TLSAlertService(context, server, savedObjectsClient);
return await statusAlertService.updateDefaultAlert();
return Promise.allSettled([
statusAlertService.updateDefaultAlert(),
tlsAlertService.updateDefaultAlert(),
]);
},
});

View file

@ -6,6 +6,7 @@
*/
import { Subject } from 'rxjs';
import { IRuleDataClient } from '@kbn/rule-registry-plugin/server';
import { registerSyntheticsTLSCheckRule } from './alert_rules/tls_rule/tls_rule';
import { registerSyntheticsStatusCheckRule } from './alert_rules/status_rule/monitor_status_rule';
import { UptimeRequestHandlerContext } from './types';
import { createSyntheticsRouteWithAuth } from './routes/create_route_with_auth';
@ -73,6 +74,16 @@ export const initSyntheticsServer = (
registerType(statusAlert);
const tlsRule = registerSyntheticsTLSCheckRule(
server,
libs,
plugins,
syntheticsMonitorClient,
ruleDataClient
);
registerType(tlsRule);
syntheticsAppStreamingApiRoutes.forEach((route) => {
const { method, streamHandler, path, options } = syntheticsRouteWrapper(
createSyntheticsRouteWithAuth(libs, route),

View file

@ -108,6 +108,9 @@ describe('getNormalizeCommonFields', () => {
status: {
enabled: statusEnabled ?? true,
},
tls: {
enabled: true,
},
},
custom_heartbeat_id: 'test-id-test-projectId-test-namespace',
enabled: true,
@ -169,6 +172,9 @@ describe('getNormalizeCommonFields', () => {
status: {
enabled: true,
},
tls: {
enabled: true,
},
},
custom_heartbeat_id: 'test-id-test-projectId-test-namespace',
enabled: true,

View file

@ -88,22 +88,35 @@ export const getNormalizeCommonFields = ({
? JSON.stringify(monitor.params)
: defaultFields[ConfigKey.PARAMS],
// picking out keys specifically, so users can't add arbitrary fields
[ConfigKey.ALERT_CONFIG]: monitor.alert
? {
...defaultFields[ConfigKey.ALERT_CONFIG],
status: {
...defaultFields[ConfigKey.ALERT_CONFIG]?.status,
enabled:
monitor.alert?.status?.enabled ??
defaultFields[ConfigKey.ALERT_CONFIG]?.status?.enabled ??
true,
},
}
: defaultFields[ConfigKey.ALERT_CONFIG],
[ConfigKey.ALERT_CONFIG]: getAlertConfig(monitor),
};
return { normalizedFields, errors };
};
const getAlertConfig = (monitor: ProjectMonitor) => {
const defaultFields = DEFAULT_COMMON_FIELDS;
return monitor.alert
? {
...defaultFields[ConfigKey.ALERT_CONFIG],
status: {
...defaultFields[ConfigKey.ALERT_CONFIG]?.status,
enabled:
monitor.alert?.status?.enabled ??
defaultFields[ConfigKey.ALERT_CONFIG]?.status?.enabled ??
true,
},
tls: {
...defaultFields[ConfigKey.ALERT_CONFIG]?.tls,
enabled:
monitor.alert?.tls?.enabled ??
defaultFields[ConfigKey.ALERT_CONFIG]?.tls?.enabled ??
true,
},
}
: defaultFields[ConfigKey.ALERT_CONFIG];
};
export const getCustomHeartbeatId = (
monitor: NormalizedProjectProps['monitor'],
projectId: string,

View file

@ -46,7 +46,7 @@ export const getNormalizeHTTPFields = ({
version,
});
// Add common erros to errors arary
// Add common errors to errors array
errors.push(...commonErrors);
/* Check if monitor has multiple urls */

View file

@ -31,6 +31,7 @@ export default function createRegisteredRuleTypeTests({ getService }: FtrProvide
'xpack.ml.anomaly_detection_alert',
'xpack.ml.anomaly_detection_jobs_health',
'xpack.synthetics.alerts.monitorStatus',
'xpack.synthetics.alerts.tls',
'xpack.uptime.alerts.monitorStatus',
'xpack.uptime.alerts.tlsCertificate',
'xpack.uptime.alerts.durationAnomaly',

View file

@ -176,6 +176,9 @@ export default function ({ getService }: FtrProviderContext) {
status: {
enabled: true,
},
tls: {
enabled: true,
},
},
'filter_journeys.match': 'check if title is present',
'filter_journeys.tags': [],
@ -358,6 +361,9 @@ export default function ({ getService }: FtrProviderContext) {
status: {
enabled: true,
},
tls: {
enabled: true,
},
},
form_monitor_type: 'http',
journey_id: journeyId,
@ -474,6 +480,9 @@ export default function ({ getService }: FtrProviderContext) {
status: {
enabled: true,
},
tls: {
enabled: true,
},
},
form_monitor_type: 'tcp',
journey_id: journeyId,
@ -580,6 +589,9 @@ export default function ({ getService }: FtrProviderContext) {
status: {
enabled: true,
},
tls: {
enabled: true,
},
},
form_monitor_type: 'icmp',
journey_id: journeyId,
@ -1920,7 +1932,13 @@ export default function ({ getService }: FtrProviderContext) {
it('project monitors - handles alert config without adding arbitrary fields', async () => {
const project = `test-project-${uuidv4()}`;
const testAlert = {
status: { enabled: false, doesnotexit: true },
status: {
enabled: false,
doesnotexit: true,
tls: {
enabled: true,
},
},
};
try {
await supertest
@ -1953,6 +1971,9 @@ export default function ({ getService }: FtrProviderContext) {
status: {
enabled: testAlert.status.enabled,
},
tls: {
enabled: true,
},
});
} finally {
await deleteMonitor(httpProjectMonitors.monitors[1].id, project);

View file

@ -105,6 +105,7 @@ export default function ({ getService }: FtrProviderContext) {
'alerting:xpack.ml.anomaly_detection_alert',
'alerting:xpack.ml.anomaly_detection_jobs_health',
'alerting:xpack.synthetics.alerts.monitorStatus',
'alerting:xpack.synthetics.alerts.tls',
'alerting:xpack.uptime.alerts.durationAnomaly',
'alerting:xpack.uptime.alerts.monitorStatus',
'alerting:xpack.uptime.alerts.tls',