[Uptime] Refactor cert alerts from batched to individual (#102138)

* refactor cert alerts from batched to individual

* remove old translations

* create new certificate alert rule type and transition old cert rule type to legacy

* update translations

* maintain legacy tls rule UI to support legacy rule editing

* update translations

* update TLS alert content, rule type id, and alert instance id schema

* remove extraneous logic and format date content

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dominique Clarke 2021-06-22 20:56:43 -04:00 committed by GitHub
parent e582549500
commit 450ababee5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 494 additions and 128 deletions

View file

@ -23486,7 +23486,6 @@
"xpack.uptime.alerts.tls.criteriaExpression.ariaLabel": "このアラートで監視されるモニターの条件を示す式",
"xpack.uptime.alerts.tls.criteriaExpression.description": "タイミング",
"xpack.uptime.alerts.tls.criteriaExpression.value": "任意のモニター",
"xpack.uptime.alerts.tls.defaultActionMessage": "期限切れになるか古くなりすぎた{count} TLS個のTLS証明書証明書を検知しました。\n\n{expiringConditionalOpen}\n期限切れになる証明書数{expiringCount}\n期限切れになる証明書{expiringCommonNameAndDate}\n{expiringConditionalClose}\n\n{agingConditionalOpen}\n古い証明書数{agingCount}\n古い証明書{agingCommonNameAndDate}\n{agingConditionalClose}\n",
"xpack.uptime.alerts.tls.description": "アップタイム監視の TLS 証明書の有効期限が近いときにアラートを発行します。",
"xpack.uptime.alerts.tls.expirationExpression.ariaLabel": "証明書有効期限の TLS アラートをトリガーするしきい値を示す式",
"xpack.uptime.alerts.tls.expirationExpression.description": "証明書が",
@ -24337,4 +24336,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。",
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
}
}
}

View file

@ -23852,7 +23852,6 @@
"xpack.uptime.alerts.tls.criteriaExpression.ariaLabel": "显示此告警监视的监测条件的表达式",
"xpack.uptime.alerts.tls.criteriaExpression.description": "当",
"xpack.uptime.alerts.tls.criteriaExpression.value": "任意监测",
"xpack.uptime.alerts.tls.defaultActionMessage": "已检测到 {count} 个即将过期或即将过时的 TLS 证书。\n\n{expiringConditionalOpen}\n即将过期的证书计数{expiringCount}\n即将过期的证书{expiringCommonNameAndDate}\n{expiringConditionalClose}\n\n{agingConditionalOpen}\n过时的证书计数{agingCount}\n过时的证书{agingCommonNameAndDate}\n{agingConditionalClose}\n",
"xpack.uptime.alerts.tls.description": "运行时间监测的 TLS 证书即将过期时告警。",
"xpack.uptime.alerts.tls.expirationExpression.ariaLabel": "显示将触发证书过期 TLS 告警的阈值的表达式",
"xpack.uptime.alerts.tls.expirationExpression.description": "具有将在",
@ -24713,4 +24712,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
}
}

View file

@ -8,7 +8,8 @@
import { ActionGroup } from '../../../alerting/common';
export type MonitorStatusActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.monitorStatus'>;
export type TLSActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>;
export type TLSLegacyActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>;
export type TLSActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tlsCertificate'>;
export type DurationAnomalyActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.durationAnomaly'>;
export const MONITOR_STATUS: MonitorStatusActionGroup = {
@ -16,8 +17,13 @@ export const MONITOR_STATUS: MonitorStatusActionGroup = {
name: 'Uptime Down Monitor',
};
export const TLS: TLSActionGroup = {
export const TLS_LEGACY: TLSLegacyActionGroup = {
id: 'xpack.uptime.alerts.actionGroups.tls',
name: 'Uptime TLS Alert (Legacy)',
};
export const TLS: TLSActionGroup = {
id: 'xpack.uptime.alerts.actionGroups.tlsCertificate',
name: 'Uptime TLS Alert',
};
@ -28,16 +34,19 @@ export const DURATION_ANOMALY: DurationAnomalyActionGroup = {
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: 'xpack.uptime.alerts.tls',
TLS_LEGACY: 'xpack.uptime.alerts.tls',
TLS: 'xpack.uptime.alerts.tlsCertificate',
DURATION_ANOMALY: 'xpack.uptime.alerts.durationAnomaly',
};

View file

@ -9,6 +9,7 @@ import { CoreStart } from 'kibana/public';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { initMonitorStatusAlertType } from './monitor_status';
import { initTlsAlertType } from './tls';
import { initTlsLegacyAlertType } from './tls_legacy';
import { ClientPluginsStart } from '../../apps/plugin';
import { initDurationAnomalyAlertType } from './duration_anomaly';
@ -20,5 +21,6 @@ export type AlertTypeInitializer = (dependenies: {
export const alertTypeInitializers: AlertTypeInitializer[] = [
initMonitorStatusAlertType,
initTlsAlertType,
initTlsLegacyAlertType,
initDurationAnomalyAlertType,
];

View file

@ -0,0 +1,32 @@
/*
* 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 { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
import { TlsTranslationsLegacy } from './translations';
import { AlertTypeInitializer } from '.';
const { defaultActionMessage, description } = TlsTranslationsLegacy;
const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert'));
export const initTlsLegacyAlertType: AlertTypeInitializer = ({
core,
plugins,
}): AlertTypeModel => ({
id: CLIENT_ALERT_TYPES.TLS_LEGACY,
iconClass: 'uptimeApp',
documentationUrl(docLinks) {
return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/uptime/${docLinks.DOC_LINK_VERSION}/uptime-alerting.html#_tls_alerts`;
},
alertParamsExpression: (params: any) => (
<TLSAlert core={core} plugins={plugins} params={params} />
),
description,
validate: () => ({ errors: {} }),
defaultActionMessage,
requiresAppContext: false,
});

View file

@ -8,14 +8,32 @@
import { i18n } from '@kbn/i18n';
export const TlsTranslations = {
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', {
defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary}
`,
values: {
commonName: '{{state.commonName}}',
issuer: '{{state.issuer}}',
summary: '{{state.summary}}',
status: '{{state.status}}',
},
}),
name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', {
defaultMessage: 'Uptime TLS (Legacy)',
}),
description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', {
defaultMessage:
'Alert when the TLS certificate of an Uptime monitor is about to expire. This alert will be deprecated in a future version.',
}),
};
export const TlsTranslationsLegacy = {
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', {
defaultMessage: `Detected {count} TLS certificates expiring or becoming too old.
{expiringConditionalOpen}
Expiring cert count: {expiringCount}
Expiring Certificates: {expiringCommonNameAndDate}
{expiringConditionalClose}
{agingConditionalOpen}
Aging cert count: {agingCount}
Aging Certificates: {agingCommonNameAndDate}

View file

@ -8,6 +8,7 @@
import { UptimeAlertTypeFactory } from './types';
import { statusCheckAlertFactory, ActionGroupIds as statusCheckActionGroup } from './status_check';
import { tlsAlertFactory, ActionGroupIds as tlsActionGroup } from './tls';
import { tlsLegacyAlertFactory, ActionGroupIds as tlsLegacyActionGroup } from './tls_legacy';
import {
durationAnomalyAlertFactory,
ActionGroupIds as durationAnomalyActionGroup,
@ -16,5 +17,6 @@ import {
export const uptimeAlertTypeFactories: [
UptimeAlertTypeFactory<statusCheckActionGroup>,
UptimeAlertTypeFactory<tlsActionGroup>,
UptimeAlertTypeFactory<tlsLegacyActionGroup>,
UptimeAlertTypeFactory<durationAnomalyActionGroup>
] = [statusCheckAlertFactory, tlsAlertFactory, durationAnomalyAlertFactory];
] = [statusCheckAlertFactory, tlsAlertFactory, tlsLegacyAlertFactory, durationAnomalyAlertFactory];

View file

@ -23,6 +23,7 @@ describe('tls alert', () => {
common_name: 'Common-One',
monitors: [{ name: 'monitor-one', id: 'monitor1' }],
sha256: 'abc',
issuer: 'Cloudflare Inc ECC CA-3',
},
{
not_after: '2020-07-18T03:15:39.000Z',
@ -30,6 +31,7 @@ describe('tls alert', () => {
common_name: 'Common-Two',
monitors: [{ name: 'monitor-two', id: 'monitor2' }],
sha256: 'bcd',
issuer: 'Cloudflare Inc ECC CA-3',
},
{
not_after: '2020-07-19T03:15:39.000Z',
@ -37,6 +39,7 @@ describe('tls alert', () => {
common_name: 'Common-Three',
monitors: [{ name: 'monitor-three', id: 'monitor3' }],
sha256: 'cde',
issuer: 'Cloudflare Inc ECC CA-3',
},
{
not_after: '2020-07-25T03:15:39.000Z',
@ -44,6 +47,7 @@ describe('tls alert', () => {
common_name: 'Common-Four',
monitors: [{ name: 'monitor-four', id: 'monitor4' }],
sha256: 'def',
issuer: 'Cloudflare Inc ECC CA-3',
},
];
});
@ -52,88 +56,66 @@ describe('tls alert', () => {
jest.clearAllMocks();
});
it('sorts expiring certs appropriately when creating summary', () => {
diffSpy.mockReturnValueOnce(900).mockReturnValueOnce(901).mockReturnValueOnce(902);
it('handles positive diffs for expired certs appropriately', () => {
diffSpy.mockReturnValueOnce(900);
const result = getCertSummary(
mockCerts,
mockCerts[0],
new Date('2020-07-20T05:00:00.000Z').valueOf(),
new Date('2019-03-01T00:00:00.000Z').valueOf()
);
expect(result).toMatchInlineSnapshot(`
Object {
"agingCommonNameAndDate": "",
"agingCount": 0,
"count": 4,
"expiringCommonNameAndDate": "Common-One, expired on 2020-07-16T03:15:39.000Z 900 days ago.; Common-Two, expired on 2020-07-18T03:15:39.000Z 901 days ago.; Common-Three, expired on 2020-07-19T03:15:39.000Z 902 days ago.",
"expiringCount": 3,
"hasAging": null,
"hasExpired": true,
}
`);
expect(result).toEqual({
commonName: mockCerts[0].common_name,
issuer: mockCerts[0].issuer,
summary: 'expired on Jul 15, 2020 EDT, 900 days ago.',
status: 'expired',
});
});
it('sorts aging certs appropriate when creating summary', () => {
diffSpy.mockReturnValueOnce(702).mockReturnValueOnce(701).mockReturnValueOnce(700);
it('handles positive diffs for agining certs appropriately', () => {
diffSpy.mockReturnValueOnce(702);
const result = getCertSummary(
mockCerts,
mockCerts[0],
new Date('2020-07-01T12:00:00.000Z').valueOf(),
new Date('2019-09-01T03:00:00.000Z').valueOf()
);
expect(result).toMatchInlineSnapshot(`
Object {
"agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 702 days ago.; Common-Three, valid since 2019-07-22T03:15:39.000Z, 701 days ago.; Common-One, valid since 2019-07-24T03:15:39.000Z, 700 days ago.",
"agingCount": 4,
"count": 4,
"expiringCommonNameAndDate": "",
"expiringCount": 0,
"hasAging": true,
"hasExpired": null,
}
`);
expect(result).toEqual({
commonName: mockCerts[0].common_name,
issuer: mockCerts[0].issuer,
summary: 'valid since Jul 23, 2019 EDT, 702 days ago.',
status: 'becoming too old',
});
});
it('handles negative diff values appropriately for aging certs', () => {
diffSpy.mockReturnValueOnce(700).mockReturnValueOnce(-90).mockReturnValueOnce(-80);
diffSpy.mockReturnValueOnce(-90);
const result = getCertSummary(
mockCerts,
mockCerts[0],
new Date('2020-07-01T12:00:00.000Z').valueOf(),
new Date('2019-09-01T03:00:00.000Z').valueOf()
);
expect(result).toMatchInlineSnapshot(`
Object {
"agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 700 days ago.; Common-Three, invalid until 2019-07-22T03:15:39.000Z, 90 days from now.; Common-One, invalid until 2019-07-24T03:15:39.000Z, 80 days from now.",
"agingCount": 4,
"count": 4,
"expiringCommonNameAndDate": "",
"expiringCount": 0,
"hasAging": true,
"hasExpired": null,
}
`);
expect(result).toEqual({
commonName: mockCerts[0].common_name,
issuer: mockCerts[0].issuer,
summary: 'invalid until Jul 23, 2019 EDT, 90 days from now.',
status: 'invalid',
});
});
it('handles negative diff values appropriately for expiring certs', () => {
diffSpy
// negative days are in the future, positive days are in the past
.mockReturnValueOnce(-96)
.mockReturnValueOnce(-94)
.mockReturnValueOnce(2);
.mockReturnValueOnce(-96);
const result = getCertSummary(
mockCerts,
mockCerts[0],
new Date('2020-07-20T05:00:00.000Z').valueOf(),
new Date('2019-03-01T00:00:00.000Z').valueOf()
);
expect(result).toMatchInlineSnapshot(`
Object {
"agingCommonNameAndDate": "",
"agingCount": 0,
"count": 4,
"expiringCommonNameAndDate": "Common-One, expires on 2020-07-16T03:15:39.000Z in 96 days.; Common-Two, expires on 2020-07-18T03:15:39.000Z in 94 days.; Common-Three, expired on 2020-07-19T03:15:39.000Z 2 days ago.",
"expiringCount": 3,
"hasAging": null,
"hasExpired": true,
}
`);
expect(result).toEqual({
commonName: mockCerts[0].common_name,
issuer: mockCerts[0].issuer,
summary: 'expires on Jul 15, 2020 EDT in 96 days.',
status: 'expiring',
});
});
});
});

View file

@ -22,71 +22,80 @@ export type ActionGroupIds = ActionGroupIdsOf<typeof TLS>;
const DEFAULT_SIZE = 20;
interface TlsAlertState {
count: number;
agingCount: number;
agingCommonNameAndDate: string;
expiringCount: number;
expiringCommonNameAndDate: string;
hasAging: true | null;
hasExpired: true | null;
commonName: string;
issuer: string;
summary: string;
status: string;
}
const sortCerts = (a: string, b: string) => new Date(a).valueOf() - new Date(b).valueOf();
interface TLSContent {
summary: string;
status?: string;
}
const mapCertsToSummaryString = (
certs: Cert[],
certLimitMessage: (cert: Cert) => string,
maxSummaryItems: number
): string =>
certs
.slice(0, maxSummaryItems)
.map((cert) => `${cert.common_name}, ${certLimitMessage(cert)}`)
.reduce((prev, cur) => (prev === '' ? cur : prev.concat(`; ${cur}`)), '');
cert: Cert,
certLimitMessage: (cert: Cert) => TLSContent
): TLSContent => certLimitMessage(cert);
const getValidAfter = ({ not_after: date }: Cert) => {
if (!date) return 'Error, missing `certificate_not_valid_after` date.';
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
? tlsTranslations.validAfterExpiredString(date, relativeDate)
: tlsTranslations.validAfterExpiringString(date, Math.abs(relativeDate));
? {
summary: tlsTranslations.validAfterExpiredString(formattedDate, relativeDate),
status: tlsTranslations.expiredLabel,
}
: {
summary: tlsTranslations.validAfterExpiringString(formattedDate, Math.abs(relativeDate)),
status: tlsTranslations.expiringLabel,
};
};
const getValidBefore = ({ not_before: date }: Cert): string => {
if (!date) return 'Error, missing `certificate_not_valid_before` date.';
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
? tlsTranslations.validBeforeExpiredString(date, relativeDate)
: tlsTranslations.validBeforeExpiringString(date, Math.abs(relativeDate));
? {
summary: tlsTranslations.validBeforeExpiredString(formattedDate, relativeDate),
status: tlsTranslations.agingLabel,
}
: {
summary: tlsTranslations.validBeforeExpiringString(formattedDate, Math.abs(relativeDate)),
status: tlsTranslations.invalidLabel,
};
};
export const getCertSummary = (
certs: Cert[],
cert: Cert,
expirationThreshold: number,
ageThreshold: number,
maxSummaryItems: number = 3
ageThreshold: number
): TlsAlertState => {
certs.sort((a, b) => sortCerts(a.not_after ?? '', b.not_after ?? ''));
const expiring = certs.filter(
(cert) => new Date(cert.not_after ?? '').valueOf() < expirationThreshold
);
const isExpiring = new Date(cert.not_after ?? '').valueOf() < expirationThreshold;
const isAging = new Date(cert.not_before ?? '').valueOf() < ageThreshold;
let content: TLSContent | null = null;
certs.sort((a, b) => sortCerts(a.not_before ?? '', b.not_before ?? ''));
const aging = certs.filter((cert) => new Date(cert.not_before ?? '').valueOf() < ageThreshold);
if (isExpiring) {
content = mapCertsToSummaryString(cert, getValidAfter);
} else if (isAging) {
content = mapCertsToSummaryString(cert, getValidBefore);
}
const { summary = '', status = '' } = content || {};
return {
count: certs.length,
agingCount: aging.length,
agingCommonNameAndDate: mapCertsToSummaryString(aging, getValidBefore, maxSummaryItems),
expiringCommonNameAndDate: mapCertsToSummaryString(expiring, getValidAfter, maxSummaryItems),
expiringCount: expiring.length,
hasAging: aging.length > 0 ? true : null,
hasExpired: expiring.length > 0 ? true : null,
commonName: cert.common_name ?? '',
issuer: cert.issuer ?? '',
summary,
status,
};
};
export const tlsAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) =>
uptimeAlertWrapper<ActionGroupIds>({
id: 'xpack.uptime.alerts.tls',
id: 'xpack.uptime.alerts.tlsCertificate',
name: tlsTranslations.alertFactoryName,
validate: {
params: schema.object({}),
@ -129,26 +138,30 @@ export const tlsAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server,
const foundCerts = total > 0;
if (foundCerts) {
const absoluteExpirationThreshold = moment()
.add(
dynamicSettings.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
'd'
)
.valueOf();
const absoluteAgeThreshold = moment()
.subtract(
dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
'd'
)
.valueOf();
const alertInstance = alertInstanceFactory(TLS.id);
const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
alertInstance.replaceState({
...updateState(state, foundCerts),
...summary,
certs.forEach((cert) => {
const absoluteExpirationThreshold = moment()
.add(
dynamicSettings.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
'd'
)
.valueOf();
const absoluteAgeThreshold = moment()
.subtract(
dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
'd'
)
.valueOf();
const alertInstance = alertInstanceFactory(
`${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`
);
const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold);
alertInstance.replaceState({
...updateState(state, foundCerts),
...summary,
});
alertInstance.scheduleActions(TLS.id);
});
alertInstance.scheduleActions(TLS.id);
}
return updateState(state, foundCerts);

View file

@ -0,0 +1,139 @@
/*
* 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 { getCertSummary } from './tls_legacy';
import { Cert } from '../../../common/runtime_types';
describe('tls alert', () => {
describe('getCertSummary', () => {
let mockCerts: Cert[];
let diffSpy: jest.SpyInstance<any, unknown[]>;
beforeEach(() => {
diffSpy = jest.spyOn(moment.prototype, 'diff');
mockCerts = [
{
not_after: '2020-07-16T03:15:39.000Z',
not_before: '2019-07-24T03:15:39.000Z',
common_name: 'Common-One',
monitors: [{ name: 'monitor-one', id: 'monitor1' }],
sha256: 'abc',
},
{
not_after: '2020-07-18T03:15:39.000Z',
not_before: '2019-07-20T03:15:39.000Z',
common_name: 'Common-Two',
monitors: [{ name: 'monitor-two', id: 'monitor2' }],
sha256: 'bcd',
},
{
not_after: '2020-07-19T03:15:39.000Z',
not_before: '2019-07-22T03:15:39.000Z',
common_name: 'Common-Three',
monitors: [{ name: 'monitor-three', id: 'monitor3' }],
sha256: 'cde',
},
{
not_after: '2020-07-25T03:15:39.000Z',
not_before: '2019-07-25T03:15:39.000Z',
common_name: 'Common-Four',
monitors: [{ name: 'monitor-four', id: 'monitor4' }],
sha256: 'def',
},
];
});
afterEach(() => {
jest.clearAllMocks();
});
it('sorts expiring certs appropriately when creating summary', () => {
diffSpy.mockReturnValueOnce(900).mockReturnValueOnce(901).mockReturnValueOnce(902);
const result = getCertSummary(
mockCerts,
new Date('2020-07-20T05:00:00.000Z').valueOf(),
new Date('2019-03-01T00:00:00.000Z').valueOf()
);
expect(result).toMatchInlineSnapshot(`
Object {
"agingCommonNameAndDate": "",
"agingCount": 0,
"count": 4,
"expiringCommonNameAndDate": "Common-One, expired on 2020-07-16T03:15:39.000Z, 900 days ago.; Common-Two, expired on 2020-07-18T03:15:39.000Z, 901 days ago.; Common-Three, expired on 2020-07-19T03:15:39.000Z, 902 days ago.",
"expiringCount": 3,
"hasAging": null,
"hasExpired": true,
}
`);
});
it('sorts aging certs appropriate when creating summary', () => {
diffSpy.mockReturnValueOnce(702).mockReturnValueOnce(701).mockReturnValueOnce(700);
const result = getCertSummary(
mockCerts,
new Date('2020-07-01T12:00:00.000Z').valueOf(),
new Date('2019-09-01T03:00:00.000Z').valueOf()
);
expect(result).toMatchInlineSnapshot(`
Object {
"agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 702 days ago.; Common-Three, valid since 2019-07-22T03:15:39.000Z, 701 days ago.; Common-One, valid since 2019-07-24T03:15:39.000Z, 700 days ago.",
"agingCount": 4,
"count": 4,
"expiringCommonNameAndDate": "",
"expiringCount": 0,
"hasAging": true,
"hasExpired": null,
}
`);
});
it('handles negative diff values appropriately for aging certs', () => {
diffSpy.mockReturnValueOnce(700).mockReturnValueOnce(-90).mockReturnValueOnce(-80);
const result = getCertSummary(
mockCerts,
new Date('2020-07-01T12:00:00.000Z').valueOf(),
new Date('2019-09-01T03:00:00.000Z').valueOf()
);
expect(result).toMatchInlineSnapshot(`
Object {
"agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 700 days ago.; Common-Three, invalid until 2019-07-22T03:15:39.000Z, 90 days from now.; Common-One, invalid until 2019-07-24T03:15:39.000Z, 80 days from now.",
"agingCount": 4,
"count": 4,
"expiringCommonNameAndDate": "",
"expiringCount": 0,
"hasAging": true,
"hasExpired": null,
}
`);
});
it('handles negative diff values appropriately for expiring certs', () => {
diffSpy
// negative days are in the future, positive days are in the past
.mockReturnValueOnce(-96)
.mockReturnValueOnce(-94)
.mockReturnValueOnce(2);
const result = getCertSummary(
mockCerts,
new Date('2020-07-20T05:00:00.000Z').valueOf(),
new Date('2019-03-01T00:00:00.000Z').valueOf()
);
expect(result).toMatchInlineSnapshot(`
Object {
"agingCommonNameAndDate": "",
"agingCount": 0,
"count": 4,
"expiringCommonNameAndDate": "Common-One, expires on 2020-07-16T03:15:39.000Z in 96 days.; Common-Two, expires on 2020-07-18T03:15:39.000Z in 94 days.; Common-Three, expired on 2020-07-19T03:15:39.000Z, 2 days ago.",
"expiringCount": 3,
"hasAging": null,
"hasExpired": true,
}
`);
});
});
});

View file

@ -0,0 +1,156 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { UptimeAlertTypeFactory } from './types';
import { updateState } from './common';
import { TLS_LEGACY } from '../../../common/constants/alerts';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { Cert, CertResult } from '../../../common/runtime_types';
import { commonStateTranslations, tlsTranslations } from './translations';
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
import { uptimeAlertWrapper } from './uptime_alert_wrapper';
import { ActionGroupIdsOf } from '../../../../alerting/common';
export type ActionGroupIds = ActionGroupIdsOf<typeof TLS_LEGACY>;
const DEFAULT_SIZE = 20;
interface TlsAlertState {
count: number;
agingCount: number;
agingCommonNameAndDate: string;
expiringCount: number;
expiringCommonNameAndDate: string;
hasAging: true | null;
hasExpired: true | null;
}
const sortCerts = (a: string, b: string) => new Date(a).valueOf() - new Date(b).valueOf();
const mapCertsToSummaryString = (
certs: Cert[],
certLimitMessage: (cert: Cert) => string,
maxSummaryItems: number
): string =>
certs
.slice(0, maxSummaryItems)
.map((cert) => `${cert.common_name}, ${certLimitMessage(cert)}`)
.reduce((prev, cur) => (prev === '' ? cur : prev.concat(`; ${cur}`)), '');
const getValidAfter = ({ not_after: date }: Cert) => {
if (!date) return 'Error, missing `certificate_not_valid_after` date.';
const relativeDate = moment().diff(date, 'days');
return relativeDate >= 0
? tlsTranslations.validAfterExpiredString(date, relativeDate)
: tlsTranslations.validAfterExpiringString(date, Math.abs(relativeDate));
};
const getValidBefore = ({ not_before: date }: Cert): string => {
if (!date) return 'Error, missing `certificate_not_valid_before` date.';
const relativeDate = moment().diff(date, 'days');
return relativeDate >= 0
? tlsTranslations.validBeforeExpiredString(date, relativeDate)
: tlsTranslations.validBeforeExpiringString(date, Math.abs(relativeDate));
};
export const getCertSummary = (
certs: Cert[],
expirationThreshold: number,
ageThreshold: number,
maxSummaryItems: number = 3
): TlsAlertState => {
certs.sort((a, b) => sortCerts(a.not_after ?? '', b.not_after ?? ''));
const expiring = certs.filter(
(cert) => new Date(cert.not_after ?? '').valueOf() < expirationThreshold
);
certs.sort((a, b) => sortCerts(a.not_before ?? '', b.not_before ?? ''));
const aging = certs.filter((cert) => new Date(cert.not_before ?? '').valueOf() < ageThreshold);
return {
count: certs.length,
agingCount: aging.length,
agingCommonNameAndDate: mapCertsToSummaryString(aging, getValidBefore, maxSummaryItems),
expiringCommonNameAndDate: mapCertsToSummaryString(expiring, getValidAfter, maxSummaryItems),
expiringCount: expiring.length,
hasAging: aging.length > 0 ? true : null,
hasExpired: expiring.length > 0 ? true : null,
};
};
export const tlsLegacyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) =>
uptimeAlertWrapper<ActionGroupIds>({
id: 'xpack.uptime.alerts.tls',
name: tlsTranslations.legacyAlertFactoryName,
validate: {
params: schema.object({}),
},
defaultActionGroupId: TLS_LEGACY.id,
actionGroups: [
{
id: TLS_LEGACY.id,
name: TLS_LEGACY.name,
},
],
actionVariables: {
context: [],
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
},
minimumLicenseRequired: 'basic',
async executor({ options, dynamicSettings, uptimeEsClient }) {
const {
services: { alertInstanceFactory },
state,
} = options;
const { certs, total }: CertResult = await libs.requests.getCerts({
uptimeEsClient,
from: DEFAULT_FROM,
to: DEFAULT_TO,
index: 0,
size: DEFAULT_SIZE,
notValidAfter: `now+${
dynamicSettings?.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
}d`,
notValidBefore: `now-${
dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
}d`,
sortBy: 'common_name',
direction: 'desc',
});
const foundCerts = total > 0;
if (foundCerts) {
const absoluteExpirationThreshold = moment()
.add(
dynamicSettings.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
'd'
)
.valueOf();
const absoluteAgeThreshold = moment()
.subtract(
dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
'd'
)
.valueOf();
const alertInstance = alertInstanceFactory(TLS_LEGACY.id);
const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
alertInstance.replaceState({
...updateState(state, foundCerts),
...summary,
});
alertInstance.scheduleActions(TLS_LEGACY.id);
}
return updateState(state, foundCerts);
},
});

View file

@ -151,6 +151,9 @@ export const tlsTranslations = {
alertFactoryName: i18n.translate('xpack.uptime.alerts.tls', {
defaultMessage: 'Uptime TLS',
}),
legacyAlertFactoryName: i18n.translate('xpack.uptime.alerts.tlsLegacy', {
defaultMessage: 'Uptime TLS (Legacy)',
}),
actionVariables: [
{
name: 'count',
@ -191,7 +194,7 @@ export const tlsTranslations = {
],
validAfterExpiredString: (date: string, relativeDate: number) =>
i18n.translate('xpack.uptime.alerts.tls.validAfterExpiredString', {
defaultMessage: `expired on {date} {relativeDate} days ago.`,
defaultMessage: `expired on {date}, {relativeDate} days ago.`,
values: {
date,
relativeDate,
@ -221,6 +224,18 @@ export const tlsTranslations = {
relativeDate,
},
}),
expiredLabel: i18n.translate('xpack.uptime.alerts.tls.expiredLabel', {
defaultMessage: 'expired',
}),
expiringLabel: i18n.translate('xpack.uptime.alerts.tls.expiringLabel', {
defaultMessage: 'expiring',
}),
agingLabel: i18n.translate('xpack.uptime.alerts.tls.agingLabel', {
defaultMessage: 'becoming too old',
}),
invalidLabel: i18n.translate('xpack.uptime.alerts.tls.invalidLabel', {
defaultMessage: 'invalid',
}),
};
export const durationAnomalyTranslations = {

View file

@ -201,7 +201,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
} = alert;
try {
expect(actions).to.eql([]);
expect(alertTypeId).to.eql('xpack.uptime.alerts.tls');
expect(alertTypeId).to.eql('xpack.uptime.alerts.tlsCertificate');
expect(consumer).to.eql('uptime');
expect(tags).to.eql(['uptime', 'certs']);
expect(params).to.eql({});