mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
97dc2ecba1
commit
8d83f64383
58 changed files with 1635 additions and 94 deletions
|
@ -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) | - |
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}',
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}}',
|
||||
},
|
||||
}
|
||||
),
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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: ',
|
||||
}),
|
||||
};
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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))}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -21,6 +21,10 @@ interface Page {
|
|||
}
|
||||
|
||||
export type CertFields =
|
||||
| 'monitorName'
|
||||
| 'locationName'
|
||||
| 'monitorType'
|
||||
| 'monitorUrl'
|
||||
| 'sha256'
|
||||
| 'sha1'
|
||||
| 'issuer'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 };
|
||||
}
|
|
@ -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}`,
|
||||
}),
|
||||
});
|
|
@ -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'
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
*/
|
||||
export const mockState: SyntheticsAppState = {
|
||||
ui: {
|
||||
alertFlyoutVisible: false,
|
||||
alertFlyoutVisible: null,
|
||||
basePath: 'yyz',
|
||||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
|
|
|
@ -21,6 +21,10 @@ interface Page {
|
|||
}
|
||||
|
||||
export type CertFields =
|
||||
| 'monitorName'
|
||||
| 'locationName'
|
||||
| 'monitorType'
|
||||
| 'monitorUrl'
|
||||
| 'sha256'
|
||||
| 'sha1'
|
||||
| 'issuer'
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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',
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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: {
|
||||
|
|
37
x-pack/plugins/synthetics/server/queries/get_certs.ts
Normal file
37
x-pack/plugins/synthetics/server/queries/get_certs.ts
Normal 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;
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue