[Uptime] TLS alerting (#63913)

* Refactor settings form event handling and modify certs fields.

* Fix/improve broken types/unit/integration/api tests.

* Modify default expiration threshold.

* Rename test vars.

* Implement PR feedback.

* Refresh snapshots, fix broken tests/types.

* Remove unnecessary state spreading.

* Add type for settings field errors.

* Refresh test snapshots.

* Improve punctuation.

* Add TLS alert type.

* Add cert API request and runtime type checking.

* Add api test for cert api.

* Add unload command to certs test.

* Extract API params interface to io-ts type.

* Add TLS alert type on server.

* WIP - add state for changing selected alert type.

* Finish adding alert type for client, add server alert summary.

* Add some state variables.

* Update certs summary function to create required values.

* Refresh test snapshots.

* Clean up message generator function.

* Add a comment.

* Update formatting for alert messages, add flags denoting presence of age/expiration data.

* Add relative date information to tls alert messages.

* Clean up more logic in certs request function.

* Fix broken unit tests.

* Move tests for common function to new file.

* Fix logic error in test and add common state fields to tls alerts.

* Extract common state field translations from status check alert.

* Add a comment.

* Add nested context navigation for uptime alert selection.

* Clean up types.

* Fix translation key typo.

* Extract translations from tls alert factory.

* Extract summary messages to translation file.

* Change default tls alert time window from 1w to 1d.

* Remove unnecessary import.

* Simplify page linking.

* Extract a non-trivial component to a dedicated file.

* Simplify create alert copy.

* Fix broken functional test.

* Fix busted types.

* Fix tls query error.

* Allow for alerts toggle button to receive a set of types to display.

* Add alerts toggle button to certs page.

* Fix copy.

* Fixup punctuation in default message to avoid double-period symbols.

* Refresh snapshots.
This commit is contained in:
Justin Kambic 2020-05-04 20:01:40 -04:00 committed by GitHub
parent 418804d6ec
commit 6d78489c14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1260 additions and 409 deletions

View file

@ -16,4 +16,13 @@ export const ACTION_GROUP_DEFINITIONS: ActionGroupDefinitions = {
id: 'xpack.uptime.alerts.actionGroups.monitorStatus',
name: 'Uptime Down Monitor',
},
TLS: {
id: 'xpack.uptime.alerts.actionGroups.tls',
name: 'Uptime TLS Alert',
},
};
export const CLIENT_ALERT_TYPES = {
MONITOR_STATUS: 'xpack.uptime.alerts.monitorStatus',
TLS: 'xpack.uptime.alerts.tls',
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { ACTION_GROUP_DEFINITIONS } from './alerts';
export * from './alerts';
export { CHART_FORMAT_LIMITS } from './chart_format_limits';
export { CLIENT_DEFAULTS } from './client_defaults';
export { CONTEXT_DEFAULTS } from './context_defaults';

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
export const UptimeCommonStateType = t.intersection([
t.partial({
currentTriggerStarted: t.string,
firstTriggeredAt: t.string,
lastTriggeredAt: t.string,
lastResolvedAt: t.string,
}),
t.type({
firstCheckedAt: t.string,
lastCheckedAt: t.string,
isTriggered: t.boolean,
}),
]);
export type UptimeCommonState = t.TypeOf<typeof UptimeCommonStateType>;

View file

@ -4,9 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
export {
StatusCheckAlertStateType,
StatusCheckAlertState,
StatusCheckExecutorParamsType,
StatusCheckExecutorParams,
} from './status_check';
export * from './common';
export * from './status_check';

View file

@ -6,22 +6,6 @@
import * as t from 'io-ts';
export const StatusCheckAlertStateType = t.intersection([
t.partial({
currentTriggerStarted: t.string,
firstTriggeredAt: t.string,
lastTriggeredAt: t.string,
lastResolvedAt: t.string,
}),
t.type({
firstCheckedAt: t.string,
lastCheckedAt: t.string,
isTriggered: t.boolean,
}),
]);
export type StatusCheckAlertState = t.TypeOf<typeof StatusCheckAlertStateType>;
export const StatusCheckExecutorParamsType = t.intersection([
t.partial({
filters: t.string,

View file

@ -15,6 +15,8 @@ export const GetCertsParamsType = t.intersection([
}),
t.partial({
search: t.string,
notValidBefore: t.string,
notValidAfter: t.string,
from: t.string,
to: t.string,
}),

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiExpression, EuiFlexItem, EuiFlexGroup, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { TlsTranslations } from './translations';
import { SettingsMessageExpressionPopover } from './settings_message_expression_popover';
interface Props {
ageThreshold?: number;
expirationThreshold?: number;
setAlertFlyoutVisible: (value: boolean) => void;
}
export const AlertTlsComponent: React.FC<Props> = props => (
<>
<EuiSpacer size="l" />
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
<EuiExpression
aria-label={TlsTranslations.criteriaAriaLabel}
color="secondary"
description={TlsTranslations.criteriaDescription}
value={TlsTranslations.criteriaValue}
/>
</EuiFlexItem>
<EuiFlexItem>
<SettingsMessageExpressionPopover
aria-label={TlsTranslations.expirationAriaLabel}
id="expiration"
description={TlsTranslations.expirationDescription}
value={TlsTranslations.expirationValue(props.expirationThreshold)}
{...props}
/>
</EuiFlexItem>
<EuiFlexItem>
<SettingsMessageExpressionPopover
aria-label={TlsTranslations.ageAriaLabel}
id="age"
description={TlsTranslations.ageDescription}
value={TlsTranslations.ageValue(props.ageThreshold)}
{...props}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
</>
);

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useDispatch, useSelector } from 'react-redux';
import React, { useCallback } from 'react';
import { AlertTlsComponent } from '../alert_tls';
import { setAlertFlyoutVisible } from '../../../../state/actions';
import { selectDynamicSettings } from '../../../../state/selectors';
export const AlertTls = () => {
const dispatch = useDispatch();
const setFlyoutVisible = useCallback((value: boolean) => dispatch(setAlertFlyoutVisible(value)), [
dispatch,
]);
const { settings } = useSelector(selectDynamicSettings);
return (
<AlertTlsComponent
ageThreshold={settings?.certThresholds?.age}
expirationThreshold={settings?.certThresholds?.expiration}
setAlertFlyoutVisible={setFlyoutVisible}
/>
);
};

View file

@ -5,5 +5,8 @@
*/
export { AlertMonitorStatus } from './alert_monitor_status';
export { ToggleAlertFlyoutButton } from './toggle_alert_flyout_button';
export {
ToggleAlertFlyoutButton,
ToggleAlertFlyoutButtonProps,
} from './toggle_alert_flyout_button';
export { UptimeAlertsFlyoutWrapper } from './uptime_alerts_flyout_wrapper';

View file

@ -6,14 +6,26 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { setAlertFlyoutVisible } from '../../../../state/actions';
import { setAlertFlyoutType, setAlertFlyoutVisible } from '../../../../state/actions';
import { ToggleAlertFlyoutButtonComponent } from '../index';
export const ToggleAlertFlyoutButton = () => {
export interface ToggleAlertFlyoutButtonProps {
alertOptions?: string[];
}
export const ToggleAlertFlyoutButton: React.FC<ToggleAlertFlyoutButtonProps> = props => {
const dispatch = useDispatch();
return (
<ToggleAlertFlyoutButtonComponent
setAlertFlyoutVisible={(value: boolean) => dispatch(setAlertFlyoutVisible(value))}
{...props}
setAlertFlyoutVisible={(value: boolean | string) => {
if (typeof value === 'string') {
dispatch(setAlertFlyoutType(value));
dispatch(setAlertFlyoutVisible(true));
} else {
dispatch(setAlertFlyoutVisible(value));
}
}}
/>
);
};

View file

@ -7,27 +7,22 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setAlertFlyoutVisible } from '../../../../state/actions';
import { selectAlertFlyoutVisibility } from '../../../../state/selectors';
import { UptimeAlertsFlyoutWrapperComponent } from '../index';
import { UptimeAlertsFlyoutWrapperComponent } from '../uptime_alerts_flyout_wrapper';
import { selectAlertFlyoutVisibility, selectAlertFlyoutType } from '../../../../state/selectors';
interface Props {
alertTypeId?: string;
canChangeTrigger?: boolean;
}
export const UptimeAlertsFlyoutWrapper = ({ alertTypeId, canChangeTrigger }: Props) => {
export const UptimeAlertsFlyoutWrapper: React.FC = () => {
const dispatch = useDispatch();
const setAddFlyoutVisibility = (value: React.SetStateAction<boolean>) =>
// @ts-ignore the value here is a boolean, and it works with the action creator function
dispatch(setAlertFlyoutVisible(value));
const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
const alertTypeId = useSelector(selectAlertFlyoutType);
return (
<UptimeAlertsFlyoutWrapperComponent
alertFlyoutVisible={alertFlyoutVisible}
alertTypeId={alertTypeId}
canChangeTrigger={canChangeTrigger}
setAlertFlyoutVisibility={setAddFlyoutVisibility}
/>
);

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiExpression, EuiPopover } from '@elastic/eui';
import { Link } from 'react-router-dom';
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { SETTINGS_ROUTE } from '../../../../common/constants';
interface SettingsMessageExpressionPopoverProps {
'aria-label': string;
description: string;
id: string;
setAlertFlyoutVisible: (value: boolean) => void;
value: string;
}
export const SettingsMessageExpressionPopover: React.FC<SettingsMessageExpressionPopoverProps> = ({
'aria-label': ariaLabel,
description,
setAlertFlyoutVisible,
value,
id,
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<EuiPopover
id={id}
anchorPosition="downLeft"
button={
<EuiExpression
aria-label={ariaLabel}
color="secondary"
description={description}
isActive={isOpen}
onClick={() => setIsOpen(!isOpen)}
value={value}
/>
}
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
>
<FormattedMessage
id="xpack.uptime.alerts.tls.settingsPageNav.text"
defaultMessage="You can edit these thresholds on the {settingsPageLink}."
values={{
settingsPageLink: (
// this link is wrapped around a span so we can also change the UI state
// and hide the alert flyout before triggering the navigation to the settings page
<Link to={SETTINGS_ROUTE} data-test-subj="xpack.uptime.alerts.tlsFlyout.linkToSettings">
<span
onClick={() => {
setAlertFlyoutVisible(false);
}}
onKeyUp={e => {
if (e.key === 'Enter') {
setAlertFlyoutVisible(false);
}
}}
>
settings page
</span>
</Link>
),
}}
/>
</EuiPopover>
);
};

View file

@ -4,27 +4,128 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import {
EuiButtonEmpty,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
EuiLink,
EuiPopover,
} from '@elastic/eui';
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { CLIENT_ALERT_TYPES } from '../../../../common/constants';
import { ToggleFlyoutTranslations } from './translations';
import { ToggleAlertFlyoutButtonProps } from './alerts_containers';
interface Props {
setAlertFlyoutVisible: (value: boolean) => void;
interface ComponentProps {
setAlertFlyoutVisible: (value: boolean | string) => void;
}
export const ToggleAlertFlyoutButtonComponent = ({ setAlertFlyoutVisible }: Props) => {
type Props = ComponentProps & ToggleAlertFlyoutButtonProps;
const ALERT_CONTEXT_MAIN_PANEL_ID = 0;
const ALERT_CONTEXT_SELECT_TYPE_PANEL_ID = 1;
export const ToggleAlertFlyoutButtonComponent: React.FC<Props> = ({
alertOptions,
setAlertFlyoutVisible,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const kibana = useKibana();
const monitorStatusAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
'data-test-subj': 'xpack.uptime.toggleAlertFlyout',
name: ToggleFlyoutTranslations.toggleMonitorStatusContent,
onClick: () => {
setAlertFlyoutVisible(CLIENT_ALERT_TYPES.MONITOR_STATUS);
setIsOpen(false);
},
};
const tlsAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
'aria-label': ToggleFlyoutTranslations.toggleTlsAriaLabel,
'data-test-subj': 'xpack.uptime.toggleTlsAlertFlyout',
name: ToggleFlyoutTranslations.toggleTlsContent,
onClick: () => {
setAlertFlyoutVisible(CLIENT_ALERT_TYPES.TLS);
setIsOpen(false);
},
};
const managementContextItem: EuiContextMenuPanelItemDescriptor = {
'aria-label': ToggleFlyoutTranslations.navigateToAlertingUIAriaLabel,
'data-test-subj': 'xpack.uptime.navigateToAlertingUi',
name: (
<EuiLink
color="text"
href={kibana.services?.application?.getUrlForApp(
'kibana#/management/kibana/triggersActions/alerts'
)}
>
<FormattedMessage
id="xpack.uptime.navigateToAlertingButton.content"
defaultMessage="Manage alerts"
/>
</EuiLink>
),
icon: 'tableOfContents',
};
let selectionItems: EuiContextMenuPanelItemDescriptor[] = [];
if (!alertOptions) {
selectionItems = [monitorStatusAlertContextMenuItem, tlsAlertContextMenuItem];
} else {
alertOptions.forEach(option => {
if (option === CLIENT_ALERT_TYPES.MONITOR_STATUS)
selectionItems.push(monitorStatusAlertContextMenuItem);
else if (option === CLIENT_ALERT_TYPES.TLS) selectionItems.push(tlsAlertContextMenuItem);
});
}
if (selectionItems.length === 1) {
selectionItems[0].icon = 'bell';
}
let panels: EuiContextMenuPanelDescriptor[];
if (selectionItems.length === 1) {
panels = [
{
id: ALERT_CONTEXT_MAIN_PANEL_ID,
title: 'main panel',
items: [...selectionItems, managementContextItem],
},
];
} else {
panels = [
{
id: ALERT_CONTEXT_MAIN_PANEL_ID,
title: 'main panel',
items: [
{
'aria-label': ToggleFlyoutTranslations.openAlertContextPanelAriaLabel,
'data-test-subj': 'xpack.uptime.openAlertContextPanel',
name: ToggleFlyoutTranslations.openAlertContextPanelLabel,
icon: 'bell',
panel: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID,
},
managementContextItem,
],
},
{
id: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID,
title: 'create alerts',
items: selectionItems,
},
];
}
return (
<EuiPopover
button={
<EuiButtonEmpty
aria-label={i18n.translate('xpack.uptime.alertsPopover.toggleButton.ariaLabel', {
defaultMessage: 'Open alert context menu',
})}
aria-label={ToggleFlyoutTranslations.toggleButtonAriaLabel}
data-test-subj="xpack.uptime.alertsPopover.toggleButton"
iconType="arrowDown"
iconSide="right"
@ -40,43 +141,7 @@ export const ToggleAlertFlyoutButtonComponent = ({ setAlertFlyoutVisible }: Prop
isOpen={isOpen}
ownFocus
>
<EuiContextMenuPanel
items={[
<EuiContextMenuItem
aria-label={i18n.translate('xpack.uptime.toggleAlertFlyout.ariaLabel', {
defaultMessage: 'Open add alert flyout',
})}
data-test-subj="xpack.uptime.toggleAlertFlyout"
key="create-alert"
icon="bell"
onClick={() => {
setAlertFlyoutVisible(true);
setIsOpen(false);
}}
>
<FormattedMessage
id="xpack.uptime.toggleAlertButton.content"
defaultMessage="Create alert"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
aria-label={i18n.translate('xpack.uptime.navigateToAlertingUi', {
defaultMessage: 'Leave Uptime and go to Alerting Management page',
})}
data-test-subj="xpack.uptime.navigateToAlertingUi"
icon="tableOfContents"
key="navigate-to-alerting"
href={kibana.services?.application?.getUrlForApp(
'kibana#/management/kibana/triggersActions/alerts'
)}
>
<FormattedMessage
id="xpack.uptime.navigateToAlertingButton.content"
defaultMessage="Manage alerts"
/>
</EuiContextMenuItem>,
]}
/>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
};

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const TlsTranslations = {
criteriaAriaLabel: i18n.translate('xpack.uptime.alerts.tls.criteriaExpression.ariaLabel', {
defaultMessage:
'An expression displaying the criteria for monitor that are watched by this alert',
}),
criteriaDescription: i18n.translate('xpack.uptime.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.uptime.alerts.tls.criteriaExpression.value', {
defaultMessage: 'any monitor',
}),
expirationAriaLabel: i18n.translate('xpack.uptime.alerts.tls.expirationExpression.ariaLabel', {
defaultMessage:
'An expression displaying the threshold that will trigger the TLS alert for certificate expiration',
}),
expirationDescription: i18n.translate(
'xpack.uptime.alerts.tls.expirationExpression.description',
{
defaultMessage: 'has a certificate expiring within',
}
),
expirationValue: (value?: number) =>
i18n.translate('xpack.uptime.alerts.tls.expirationExpression.value', {
defaultMessage: '{value} days',
values: { value },
}),
ageAriaLabel: i18n.translate('xpack.uptime.alerts.tls.ageExpression.ariaLabel', {
defaultMessage:
'An expressing displaying the threshold that will trigger the TLS alert for old certificates',
}),
ageDescription: i18n.translate('xpack.uptime.alerts.tls.ageExpression.description', {
defaultMessage: 'or older than',
}),
ageValue: (value?: number) =>
i18n.translate('xpack.uptime.alerts.tls.ageExpression.value', {
defaultMessage: '{value} days',
values: { value },
}),
};
export const ToggleFlyoutTranslations = {
toggleButtonAriaLabel: i18n.translate('xpack.uptime.alertsPopover.toggleButton.ariaLabel', {
defaultMessage: 'Open alert context menu',
}),
openAlertContextPanelAriaLabel: i18n.translate('xpack.uptime.openAlertContextPanel.ariaLabel', {
defaultMessage: 'Open the alert context panel so you can choose an alert type',
}),
openAlertContextPanelLabel: i18n.translate('xpack.uptime.openAlertContextPanel.label', {
defaultMessage: 'Create alert',
}),
toggleTlsAriaLabel: i18n.translate('xpack.uptime.toggleTlsAlertButton.ariaLabel', {
defaultMessage: 'Open TLS alert flyout',
}),
toggleTlsContent: i18n.translate('xpack.uptime.toggleTlsAlertButton.content', {
defaultMessage: 'TLS alert',
}),
toggleMonitorStatusAriaLabel: i18n.translate('xpack.uptime.toggleAlertFlyout.ariaLabel', {
defaultMessage: 'Open add alert flyout',
}),
toggleMonitorStatusContent: i18n.translate('xpack.uptime.toggleAlertButton.content', {
defaultMessage: 'Monitor status alert',
}),
navigateToAlertingUIAriaLabel: i18n.translate('xpack.uptime.navigateToAlertingUi', {
defaultMessage: 'Leave Uptime and go to Alerting Management page',
}),
navigateToAlertingButtonContent: i18n.translate('xpack.uptime.navigateToAlertingButton.content', {
defaultMessage: 'Manage alerts',
}),
};

View file

@ -10,14 +10,12 @@ import { AlertAdd } from '../../../../../../plugins/triggers_actions_ui/public';
interface Props {
alertFlyoutVisible: boolean;
alertTypeId?: string;
canChangeTrigger?: boolean;
setAlertFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
}
export const UptimeAlertsFlyoutWrapperComponent = ({
alertFlyoutVisible,
alertTypeId,
canChangeTrigger,
setAlertFlyoutVisibility,
}: Props) => (
<AlertAdd
@ -25,6 +23,8 @@ export const UptimeAlertsFlyoutWrapperComponent = ({
consumer="uptime"
setAddFlyoutVisibility={setAlertFlyoutVisibility}
alertTypeId={alertTypeId}
canChangeTrigger={canChangeTrigger}
// if we don't have an alert type pre-specified, we need to
// let the user choose
canChangeTrigger={!alertTypeId}
/>
);

View file

@ -4,20 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { connect } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import React, { useCallback } from 'react';
import { DataPublicPluginSetup } from 'src/plugins/data/public';
import { OverviewPageComponent } from '../../pages/overview';
import { selectIndexPattern } from '../../state/selectors';
import { AppState } from '../../state';
import { setEsKueryString } from '../../state/actions';
interface DispatchProps {
setEsKueryFilters: typeof setEsKueryString;
export interface OverviewPageProps {
autocomplete: DataPublicPluginSetup['autocomplete'];
}
const mapDispatchToProps = (dispatch: any): DispatchProps => ({
setEsKueryFilters: (esFilters: string) => dispatch(setEsKueryString(esFilters)),
});
const mapStateToProps = (state: AppState) => ({ ...selectIndexPattern(state) });
export const OverviewPage = connect(mapStateToProps, mapDispatchToProps)(OverviewPageComponent);
export const OverviewPage: React.FC<OverviewPageProps> = props => {
const dispatch = useDispatch();
const setEsKueryFilters = useCallback(
(esFilters: string) => dispatch(setEsKueryString(esFilters)),
[dispatch]
);
const indexPattern = useSelector(selectIndexPattern);
return (
<OverviewPageComponent setEsKueryFilters={setEsKueryFilters} {...indexPattern} {...props} />
);
};

View file

@ -6,7 +6,11 @@
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { initMonitorStatusAlertType } from './monitor_status';
import { initTlsAlertType } from './tls';
export type AlertTypeInitializer = (dependenies: { autocomplete: any }) => AlertTypeModel;
export const alertTypeInitializers: AlertTypeInitializer[] = [initMonitorStatusAlertType];
export const alertTypeInitializers: AlertTypeInitializer[] = [
initMonitorStatusAlertType,
initTlsAlertType,
];

View file

@ -12,6 +12,8 @@ import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { AlertTypeInitializer } from '.';
import { StatusCheckExecutorParamsType } from '../../../common/runtime_types';
import { AlertMonitorStatus } from '../../components/overview/alerts/alerts_containers';
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
import { MonitorStatusTranslations } from './translations';
export const validate = (alertParams: any) => {
const errors: Record<string, any> = {};
@ -52,15 +54,15 @@ export const validate = (alertParams: any) => {
return { errors };
};
const { name, defaultActionMessage } = MonitorStatusTranslations;
export const initMonitorStatusAlertType: AlertTypeInitializer = ({
autocomplete,
}): AlertTypeModel => ({
id: 'xpack.uptime.alerts.monitorStatus',
name: 'Uptime monitor status',
id: CLIENT_ALERT_TYPES.MONITOR_STATUS,
name,
iconClass: 'uptimeApp',
alertParamsExpression: params => {
return <AlertMonitorStatus {...params} autocomplete={autocomplete} />;
},
alertParamsExpression: params => <AlertMonitorStatus {...params} autocomplete={autocomplete} />,
validate,
defaultActionMessage: `{{context.message}}\nLast triggered at: {{state.lastTriggeredAt}}\n{{context.downMonitorsWithGeo}}`,
defaultActionMessage,
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
import { TlsTranslations } from './translations';
import { AlertTypeInitializer } from '.';
import { AlertTls } from '../../components/overview/alerts/alerts_containers/alert_tls';
const { name, defaultActionMessage } = TlsTranslations;
export const initTlsAlertType: AlertTypeInitializer = (): AlertTypeModel => ({
id: CLIENT_ALERT_TYPES.TLS,
iconClass: 'uptimeApp',
alertParamsExpression: () => <AlertTls />,
name,
validate: () => ({ errors: {} }),
defaultActionMessage,
});

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const MonitorStatusTranslations = {
defaultActionMessage: i18n.translate('xpack.uptime.alerts.monitorStatus.defaultActionMessage', {
defaultMessage: '{contextMessage}\nLast triggered at: {lastTriggered}\n{downMonitors}',
values: {
contextMessage: '{{context.message}}',
lastTriggered: '{{state.lastTriggeredAt}}',
downMonitors: '{{context.downMonitorsWithGeo}}',
},
}),
name: i18n.translate('xpack.uptime.alerts.monitorStatus.clientName', {
defaultMessage: 'Uptime monitor status',
}),
};
export const TlsTranslations = {
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', {
defaultMessage: `Detected {count} TLS certificates expiring or becoming too old.
{expiringConditionalOpen}
Expiring cert count: {expiringCount}
Expiring Certificates: {expiringCommonNameAndDate}
{expiringConditionalClose}
{agingConditionalOpen}
Aging cert count: {agingCount}
Aging Certificates: {agingCommonNameAndDate}
{agingConditionalClose}
`,
values: {
count: '{{state.count}}',
expiringCount: '{{state.expiringCount}}',
expiringCommonNameAndDate: '{{state.expiringCommonNameAndDate}}',
expiringConditionalOpen: '{{#state.hasExpired}}',
expiringConditionalClose: '{{/state.hasExpired}}',
agingCount: '{{state.agingCount}}',
agingCommonNameAndDate: '{{state.agingCommonNameAndDate}}',
agingConditionalOpen: '{{#state.hasAging}}',
agingConditionalClose: '{{/state.hasAging}}',
},
}),
name: i18n.translate('xpack.uptime.alerts.tls.clientName', {
defaultMessage: 'Uptime TLS',
}),
};

View file

@ -19,13 +19,14 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { useTrackPageview } from '../../../observability/public';
import { PageHeader } from './page_header';
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
import { OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../../common/constants';
import { OVERVIEW_ROUTE, SETTINGS_ROUTE, CLIENT_ALERT_TYPES } from '../../common/constants';
import { getDynamicSettings } from '../state/actions/dynamic_settings';
import { UptimeRefreshContext } from '../contexts';
import * as labels from './translations';
import { UptimePage, useUptimeTelemetry } from '../hooks';
import { certificatesSelector, getCertificatesAction } from '../state/certificates/certificates';
import { CertificateList, CertificateSearch, CertSort } from '../components/certificates';
import { ToggleAlertFlyoutButton } from '../components/overview/alerts/alerts_containers';
const DEFAULT_PAGE_SIZE = 10;
const LOCAL_STORAGE_KEY = 'xpack.uptime.certList.pageSize';
@ -83,6 +84,9 @@ export const CertificatesPage: React.FC = () => {
</EuiButtonEmpty>
</Link>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ToggleAlertFlyoutButton alertOptions={[CLIENT_ALERT_TYPES.TLS]} />
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
<Link to={SETTINGS_ROUTE} data-test-subj="uptimeCertificatesToOverviewLink">
<EuiButtonEmpty size="s" color="primary" iconType="gear">

View file

@ -11,22 +11,20 @@ import { i18n } from '@kbn/i18n';
import { useUptimeTelemetry, UptimePage, useGetUrlParams } from '../hooks';
import { stringifyUrlParams } from '../lib/helper/stringify_url_params';
import { PageHeader } from './page_header';
import { DataPublicPluginSetup, IIndexPattern } from '../../../../../src/plugins/data/public';
import { IIndexPattern } from '../../../../../src/plugins/data/public';
import { useUpdateKueryString } from '../hooks';
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
import { useTrackPageview } from '../../../observability/public';
import { MonitorList } from '../components/overview/monitor_list/monitor_list_container';
import { EmptyState, FilterGroup, KueryBar, ParsingErrorCallout } from '../components/overview';
import { StatusPanel } from '../components/overview/status_panel';
import { OverviewPageProps } from '../components/overview/overview_container';
interface OverviewPageProps {
autocomplete: DataPublicPluginSetup['autocomplete'];
interface Props extends OverviewPageProps {
indexPattern: IIndexPattern | null;
setEsKueryFilters: (esFilters: string) => void;
}
type Props = OverviewPageProps;
const EuiFlexItemStyled = styled(EuiFlexItem)`
&& {
min-width: 598px;

View file

@ -12,7 +12,9 @@ export interface PopoverState {
export type UiPayload = PopoverState & string & number & Map<string, string[]>;
export const setAlertFlyoutVisible = createAction<boolean>('TOGGLE ALERT FLYOUT');
export const setAlertFlyoutVisible = createAction<boolean | undefined>('TOGGLE ALERT FLYOUT');
export const setAlertFlyoutType = createAction<string>('SET ALERT FLYOUT TYPE');
export const setBasePath = createAction<string>('SET BASE PATH');

View file

@ -12,11 +12,13 @@ import {
setEsKueryString,
triggerAppRefresh,
UiPayload,
setAlertFlyoutType,
setAlertFlyoutVisible,
} from '../actions';
export interface UiState {
alertFlyoutVisible: boolean;
alertFlyoutType?: string;
basePath: string;
esKuery: string;
integrationsPopoverOpen: PopoverState | null;
@ -57,6 +59,11 @@ export const uiReducer = handleActions<UiState, UiPayload>(
...state,
esKuery: action.payload as string,
}),
[String(setAlertFlyoutType)]: (state, action: Action<string>) => ({
...state,
alertFlyoutType: action.payload,
}),
},
initialState
);

View file

@ -91,6 +91,8 @@ export const selectDurationLines = ({ monitorDuration }: AppState) => {
export const selectAlertFlyoutVisibility = ({ ui: { alertFlyoutVisible } }: AppState) =>
alertFlyoutVisible;
export const selectAlertFlyoutType = ({ ui: { alertFlyoutType } }: AppState) => alertFlyoutType;
export const selectMonitorStatusAlert = ({ indexPattern, overviewFilters, ui }: AppState) => ({
filters: ui.esKuery,
indexPattern: indexPattern.index_pattern,

View file

@ -105,10 +105,7 @@ const Application = (props: UptimeAppProps) => {
<UptimeAlertsContextProvider>
<EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp">
<main>
<UptimeAlertsFlyoutWrapper
alertTypeId="xpack.uptime.alerts.monitorStatus"
canChangeTrigger={false}
/>
<UptimeAlertsFlyoutWrapper />
<PageRouter autocomplete={plugins.data.autocomplete} />
</main>
</EuiPage>

View file

@ -0,0 +1,180 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { updateState } from '../common';
describe('updateState', () => {
let spy: jest.SpyInstance<string, []>;
beforeEach(() => {
spy = jest.spyOn(Date.prototype, 'toISOString');
});
afterEach(() => {
jest.clearAllMocks();
});
it('sets initial state values', () => {
spy.mockImplementation(() => 'foo date string');
const result = updateState({}, false);
expect(spy).toHaveBeenCalledTimes(1);
expect(result).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "foo date string",
"firstTriggeredAt": undefined,
"isTriggered": false,
"lastCheckedAt": "foo date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": undefined,
}
`);
});
it('updates the correct field in subsequent calls', () => {
spy
.mockImplementationOnce(() => 'first date string')
.mockImplementationOnce(() => 'second date string');
const firstState = updateState({}, false);
const secondState = updateState(firstState, true);
expect(spy).toHaveBeenCalledTimes(2);
expect(firstState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "first date string",
"firstTriggeredAt": undefined,
"isTriggered": false,
"lastCheckedAt": "first date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": undefined,
}
`);
expect(secondState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": "second date string",
"firstCheckedAt": "first date string",
"firstTriggeredAt": "second date string",
"isTriggered": true,
"lastCheckedAt": "second date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": "second date string",
}
`);
});
it('correctly marks resolution times', () => {
spy
.mockImplementationOnce(() => 'first date string')
.mockImplementationOnce(() => 'second date string')
.mockImplementationOnce(() => 'third date string');
const firstState = updateState({}, true);
const secondState = updateState(firstState, true);
const thirdState = updateState(secondState, false);
expect(spy).toHaveBeenCalledTimes(3);
expect(firstState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": "first date string",
"firstCheckedAt": "first date string",
"firstTriggeredAt": "first date string",
"isTriggered": true,
"lastCheckedAt": "first date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": "first date string",
}
`);
expect(secondState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": "first date string",
"firstCheckedAt": "first date string",
"firstTriggeredAt": "first date string",
"isTriggered": true,
"lastCheckedAt": "second date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": "second date string",
}
`);
expect(thirdState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "first date string",
"firstTriggeredAt": "first date string",
"isTriggered": false,
"lastCheckedAt": "third date string",
"lastResolvedAt": "third date string",
"lastTriggeredAt": "second date string",
}
`);
});
it('correctly marks state fields across multiple triggers/resolutions', () => {
spy
.mockImplementationOnce(() => 'first date string')
.mockImplementationOnce(() => 'second date string')
.mockImplementationOnce(() => 'third date string')
.mockImplementationOnce(() => 'fourth date string')
.mockImplementationOnce(() => 'fifth date string');
const firstState = updateState({}, false);
const secondState = updateState(firstState, true);
const thirdState = updateState(secondState, false);
const fourthState = updateState(thirdState, true);
const fifthState = updateState(fourthState, false);
expect(spy).toHaveBeenCalledTimes(5);
expect(firstState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "first date string",
"firstTriggeredAt": undefined,
"isTriggered": false,
"lastCheckedAt": "first date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": undefined,
}
`);
expect(secondState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": "second date string",
"firstCheckedAt": "first date string",
"firstTriggeredAt": "second date string",
"isTriggered": true,
"lastCheckedAt": "second date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": "second date string",
}
`);
expect(thirdState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "first date string",
"firstTriggeredAt": "second date string",
"isTriggered": false,
"lastCheckedAt": "third date string",
"lastResolvedAt": "third date string",
"lastTriggeredAt": "second date string",
}
`);
expect(fourthState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": "fourth date string",
"firstCheckedAt": "first date string",
"firstTriggeredAt": "second date string",
"isTriggered": true,
"lastCheckedAt": "fourth date string",
"lastResolvedAt": "third date string",
"lastTriggeredAt": "fourth date string",
}
`);
expect(fifthState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "first date string",
"firstTriggeredAt": "second date string",
"isTriggered": false,
"lastCheckedAt": "fifth date string",
"lastResolvedAt": "fifth date string",
"lastTriggeredAt": "fourth date string",
}
`);
});
});

View file

@ -7,7 +7,6 @@
import {
contextMessage,
uniqueMonitorIds,
updateState,
statusCheckAlertFactory,
fullListByIdAndLocation,
} from '../status_check';
@ -337,179 +336,6 @@ describe('status check alert', () => {
});
});
describe('updateState', () => {
let spy: jest.SpyInstance<string, []>;
beforeEach(() => {
spy = jest.spyOn(Date.prototype, 'toISOString');
});
afterEach(() => {
jest.clearAllMocks();
});
it('sets initial state values', () => {
spy.mockImplementation(() => 'foo date string');
const result = updateState({}, false);
expect(spy).toHaveBeenCalledTimes(1);
expect(result).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "foo date string",
"firstTriggeredAt": undefined,
"isTriggered": false,
"lastCheckedAt": "foo date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": undefined,
}
`);
});
it('updates the correct field in subsequent calls', () => {
spy
.mockImplementationOnce(() => 'first date string')
.mockImplementationOnce(() => 'second date string');
const firstState = updateState({}, false);
const secondState = updateState(firstState, true);
expect(spy).toHaveBeenCalledTimes(2);
expect(firstState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "first date string",
"firstTriggeredAt": undefined,
"isTriggered": false,
"lastCheckedAt": "first date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": undefined,
}
`);
expect(secondState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": "second date string",
"firstCheckedAt": "first date string",
"firstTriggeredAt": "second date string",
"isTriggered": true,
"lastCheckedAt": "second date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": "second date string",
}
`);
});
it('correctly marks resolution times', () => {
spy
.mockImplementationOnce(() => 'first date string')
.mockImplementationOnce(() => 'second date string')
.mockImplementationOnce(() => 'third date string');
const firstState = updateState({}, true);
const secondState = updateState(firstState, true);
const thirdState = updateState(secondState, false);
expect(spy).toHaveBeenCalledTimes(3);
expect(firstState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": "first date string",
"firstCheckedAt": "first date string",
"firstTriggeredAt": "first date string",
"isTriggered": true,
"lastCheckedAt": "first date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": "first date string",
}
`);
expect(secondState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": "first date string",
"firstCheckedAt": "first date string",
"firstTriggeredAt": "first date string",
"isTriggered": true,
"lastCheckedAt": "second date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": "second date string",
}
`);
expect(thirdState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "first date string",
"firstTriggeredAt": "first date string",
"isTriggered": false,
"lastCheckedAt": "third date string",
"lastResolvedAt": "third date string",
"lastTriggeredAt": "second date string",
}
`);
});
it('correctly marks state fields across multiple triggers/resolutions', () => {
spy
.mockImplementationOnce(() => 'first date string')
.mockImplementationOnce(() => 'second date string')
.mockImplementationOnce(() => 'third date string')
.mockImplementationOnce(() => 'fourth date string')
.mockImplementationOnce(() => 'fifth date string');
const firstState = updateState({}, false);
const secondState = updateState(firstState, true);
const thirdState = updateState(secondState, false);
const fourthState = updateState(thirdState, true);
const fifthState = updateState(fourthState, false);
expect(spy).toHaveBeenCalledTimes(5);
expect(firstState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "first date string",
"firstTriggeredAt": undefined,
"isTriggered": false,
"lastCheckedAt": "first date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": undefined,
}
`);
expect(secondState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": "second date string",
"firstCheckedAt": "first date string",
"firstTriggeredAt": "second date string",
"isTriggered": true,
"lastCheckedAt": "second date string",
"lastResolvedAt": undefined,
"lastTriggeredAt": "second date string",
}
`);
expect(thirdState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "first date string",
"firstTriggeredAt": "second date string",
"isTriggered": false,
"lastCheckedAt": "third date string",
"lastResolvedAt": "third date string",
"lastTriggeredAt": "second date string",
}
`);
expect(fourthState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": "fourth date string",
"firstCheckedAt": "first date string",
"firstTriggeredAt": "second date string",
"isTriggered": true,
"lastCheckedAt": "fourth date string",
"lastResolvedAt": "third date string",
"lastTriggeredAt": "fourth date string",
}
`);
expect(fifthState).toMatchInlineSnapshot(`
Object {
"currentTriggerStarted": undefined,
"firstCheckedAt": "first date string",
"firstTriggeredAt": "second date string",
"isTriggered": false,
"lastCheckedAt": "fifth date string",
"lastResolvedAt": "fifth date string",
"lastTriggeredAt": "fourth date string",
}
`);
});
});
describe('uniqueMonitorIds', () => {
let items: GetMonitorStatusResult[];
beforeEach(() => {

View file

@ -0,0 +1,147 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment';
import { getCertSummary } from '../tls';
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,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { isRight } from 'fp-ts/lib/Either';
import { UptimeCommonState, UptimeCommonStateType } from '../../../common/runtime_types';
export type UpdateUptimeAlertState = (
state: Record<string, any>,
isTriggeredNow: boolean
) => UptimeCommonState;
export const updateState: UpdateUptimeAlertState = (state, isTriggeredNow) => {
const now = new Date().toISOString();
const decoded = UptimeCommonStateType.decode(state);
if (!isRight(decoded)) {
const triggerVal = isTriggeredNow ? now : undefined;
return {
currentTriggerStarted: triggerVal,
firstCheckedAt: now,
firstTriggeredAt: triggerVal,
isTriggered: isTriggeredNow,
lastTriggeredAt: triggerVal,
lastCheckedAt: now,
lastResolvedAt: undefined,
};
}
const {
currentTriggerStarted,
firstCheckedAt,
firstTriggeredAt,
lastTriggeredAt,
// this is the stale trigger status, we're naming it `wasTriggered`
// to differentiate it from the `isTriggeredNow` param
isTriggered: wasTriggered,
lastResolvedAt,
} = decoded.right;
let cts: string | undefined;
if (isTriggeredNow && !currentTriggerStarted) {
cts = now;
} else if (isTriggeredNow) {
cts = currentTriggerStarted;
}
return {
currentTriggerStarted: cts,
firstCheckedAt: firstCheckedAt ?? now,
firstTriggeredAt: isTriggeredNow && !firstTriggeredAt ? now : firstTriggeredAt,
lastCheckedAt: now,
lastTriggeredAt: isTriggeredNow ? now : lastTriggeredAt,
lastResolvedAt: !isTriggeredNow && wasTriggered ? now : lastResolvedAt,
isTriggered: isTriggeredNow,
};
};

View file

@ -6,5 +6,9 @@
import { UptimeAlertTypeFactory } from './types';
import { statusCheckAlertFactory } from './status_check';
import { tlsAlertFactory } from './tls';
export const uptimeAlertTypeFactories: UptimeAlertTypeFactory[] = [statusCheckAlertFactory];
export const uptimeAlertTypeFactories: UptimeAlertTypeFactory[] = [
statusCheckAlertFactory,
tlsAlertFactory,
];

View file

@ -11,13 +11,11 @@ import { i18n } from '@kbn/i18n';
import { AlertExecutorOptions } from '../../../../alerting/server';
import { UptimeAlertTypeFactory } from './types';
import { GetMonitorStatusResult } from '../requests';
import {
StatusCheckExecutorParamsType,
StatusCheckAlertStateType,
StatusCheckAlertState,
} from '../../../common/runtime_types';
import { StatusCheckExecutorParamsType } from '../../../common/runtime_types';
import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants';
import { savedObjectsAdapter } from '../saved_objects';
import { updateState } from './common';
import { commonStateTranslations } from './translations';
const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS;
@ -34,7 +32,7 @@ export const uniqueMonitorIds = (items: GetMonitorStatusResult[]): Set<string> =
/**
* Generates a message to include in contexts of alerts.
* @param monitors the list of monitors to include in the message
* @param max
* @param max the maximum number of items the summary should contain
*/
export const contextMessage = (monitorIds: string[], max: number): string => {
const MIN = 2;
@ -122,58 +120,11 @@ export const fullListByIdAndLocation = (
);
};
export const updateState = (
state: Record<string, any>,
isTriggeredNow: boolean
): StatusCheckAlertState => {
const now = new Date().toISOString();
const decoded = StatusCheckAlertStateType.decode(state);
if (!isRight(decoded)) {
const triggerVal = isTriggeredNow ? now : undefined;
return {
currentTriggerStarted: triggerVal,
firstCheckedAt: now,
firstTriggeredAt: triggerVal,
isTriggered: isTriggeredNow,
lastTriggeredAt: triggerVal,
lastCheckedAt: now,
lastResolvedAt: undefined,
};
}
const {
currentTriggerStarted,
firstCheckedAt,
firstTriggeredAt,
lastTriggeredAt,
// this is the stale trigger status, we're naming it `wasTriggered`
// to differentiate it from the `isTriggeredNow` param
isTriggered: wasTriggered,
lastResolvedAt,
} = decoded.right;
let cts: string | undefined;
if (isTriggeredNow && !currentTriggerStarted) {
cts = now;
} else if (isTriggeredNow) {
cts = currentTriggerStarted;
}
return {
currentTriggerStarted: cts,
firstCheckedAt: firstCheckedAt ?? now,
firstTriggeredAt: isTriggeredNow && !firstTriggeredAt ? now : firstTriggeredAt,
lastCheckedAt: now,
lastTriggeredAt: isTriggeredNow ? now : lastTriggeredAt,
lastResolvedAt: !isTriggeredNow && wasTriggered ? now : lastResolvedAt,
isTriggered: isTriggeredNow,
};
};
// Right now the maximum number of monitors shown in the message is hardcoded here.
// we might want to make this a parameter in the future
const DEFAULT_MAX_MESSAGE_ROWS = 3;
export const statusCheckAlertFactory: UptimeAlertTypeFactory = (server, libs) => ({
export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({
id: 'xpack.uptime.alerts.monitorStatus',
name: i18n.translate('xpack.uptime.alerts.monitorStatus', {
defaultMessage: 'Uptime monitor status',
@ -218,72 +169,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (server, libs) =>
),
},
],
state: [
{
name: 'firstCheckedAt',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.firstCheckedAt',
{
defaultMessage: 'Timestamp indicating when this alert first checked',
}
),
},
{
name: 'firstTriggeredAt',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.firstTriggeredAt',
{
defaultMessage: 'Timestamp indicating when the alert first triggered',
}
),
},
{
name: 'currentTriggerStarted',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.currentTriggerStarted',
{
defaultMessage:
'Timestamp indicating when the current trigger state began, if alert is triggered',
}
),
},
{
name: 'isTriggered',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.isTriggered',
{
defaultMessage: `Flag indicating if the alert is currently triggering`,
}
),
},
{
name: 'lastCheckedAt',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastCheckedAt',
{
defaultMessage: `Timestamp indicating the alert's most recent check time`,
}
),
},
{
name: 'lastResolvedAt',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastResolvedAt',
{
defaultMessage: `Timestamp indicating the most recent resolution time for this alert`,
}
),
},
{
name: 'lastTriggeredAt',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastTriggeredAt',
{
defaultMessage: `Timestamp indicating the alert's most recent trigger time`,
}
),
},
],
state: [...commonStateTranslations],
},
async executor(options: AlertExecutorOptions) {
const { params: rawParams } = options;

View file

@ -0,0 +1,152 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment';
import { schema } from '@kbn/config-schema';
import { UptimeAlertTypeFactory } from './types';
import { savedObjectsAdapter } from '../saved_objects';
import { updateState } from './common';
import { ACTION_GROUP_DEFINITIONS, DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { Cert, CertResult } from '../../../common/runtime_types';
import { commonStateTranslations, tlsTranslations } from './translations';
const { TLS } = ACTION_GROUP_DEFINITIONS;
const DEFAULT_FROM = 'now-1d';
const DEFAULT_TO = 'now';
const DEFAULT_INDEX = 0;
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 tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({
id: 'xpack.uptime.alerts.tls',
name: tlsTranslations.alertFactoryName,
validate: {
params: schema.object({}),
},
defaultActionGroupId: TLS.id,
actionGroups: [
{
id: TLS.id,
name: TLS.name,
},
],
actionVariables: {
context: [],
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
},
async executor(options) {
const {
services: { alertInstanceFactory, callCluster, savedObjectsClient },
state,
} = options;
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
const { certs, total }: CertResult = await libs.requests.getCerts({
callES: callCluster,
dynamicSettings,
from: DEFAULT_FROM,
to: DEFAULT_TO,
index: DEFAULT_INDEX,
size: DEFAULT_SIZE,
notValidAfter: `now+${dynamicSettings.certThresholds?.expiration ??
DYNAMIC_SETTINGS_DEFAULTS.certThresholds?.expiration}d`,
notValidBefore: `now-${dynamicSettings.certThresholds?.age ??
DYNAMIC_SETTINGS_DEFAULTS.certThresholds?.age}d`,
sortBy: 'common_name',
direction: 'desc',
});
const foundCerts = total > 0;
if (foundCerts) {
const absoluteExpirationThreshold = moment()
.add(
dynamicSettings.certThresholds?.expiration ??
DYNAMIC_SETTINGS_DEFAULTS.certThresholds?.expiration,
'd'
)
.valueOf();
const absoluteAgeThreshold = moment()
.subtract(
dynamicSettings.certThresholds?.age ?? DYNAMIC_SETTINGS_DEFAULTS.certThresholds?.age,
'd'
)
.valueOf();
const alertInstance = alertInstanceFactory(TLS.id);
const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
alertInstance.replaceState({
...updateState(state, foundCerts),
...summary,
});
alertInstance.scheduleActions(TLS.id);
}
return updateState(state, foundCerts);
},
});

View file

@ -0,0 +1,150 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const commonStateTranslations = [
{
name: 'firstCheckedAt',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.firstCheckedAt',
{
defaultMessage: 'Timestamp indicating when this alert first checked',
}
),
},
{
name: 'firstTriggeredAt',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.firstTriggeredAt',
{
defaultMessage: 'Timestamp indicating when the alert first triggered',
}
),
},
{
name: 'currentTriggerStarted',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.currentTriggerStarted',
{
defaultMessage:
'Timestamp indicating when the current trigger state began, if alert is triggered',
}
),
},
{
name: 'isTriggered',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.isTriggered',
{
defaultMessage: `Flag indicating if the alert is currently triggering`,
}
),
},
{
name: 'lastCheckedAt',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastCheckedAt',
{
defaultMessage: `Timestamp indicating the alert's most recent check time`,
}
),
},
{
name: 'lastResolvedAt',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastResolvedAt',
{
defaultMessage: `Timestamp indicating the most recent resolution time for this alert`,
}
),
},
{
name: 'lastTriggeredAt',
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.state.lastTriggeredAt',
{
defaultMessage: `Timestamp indicating the alert's most recent trigger time`,
}
),
},
];
export const tlsTranslations = {
alertFactoryName: i18n.translate('xpack.uptime.alerts.tls', {
defaultMessage: 'Uptime TLS',
}),
actionVariables: [
{
name: 'count',
description: i18n.translate('xpack.uptime.alerts.tls.actionVariables.state.count', {
defaultMessage: 'The number of certs detected by the alert executor',
}),
},
{
name: 'expiringCount',
description: i18n.translate('xpack.uptime.alerts.tls.actionVariables.state.expiringCount', {
defaultMessage: 'The number of expiring certs detected by the alert.',
}),
},
{
name: 'expiringCommonNameAndDate',
description: i18n.translate(
'xpack.uptime.alerts.tls.actionVariables.state.expiringCommonNameAndDate',
{
defaultMessage: 'The common names and expiration date/time of the detected certs',
}
),
},
{
name: 'agingCount',
description: i18n.translate('xpack.uptime.alerts.tls.actionVariables.state.agingCount', {
defaultMessage: 'The number of detected certs that are becoming too old.',
}),
},
{
name: 'agingCommonNameAndDate',
description: i18n.translate(
'xpack.uptime.alerts.tls.actionVariables.state.agingCommonNameAndDate',
{
defaultMessage: 'The common names and expiration date/time of the detected certs.',
}
),
},
],
validAfterExpiredString: (date: string, relativeDate: number) =>
i18n.translate('xpack.uptime.alerts.tls.validAfterExpiredString', {
defaultMessage: `expired on {date} {relativeDate} days ago.`,
values: {
date,
relativeDate,
},
}),
validAfterExpiringString: (date: string, relativeDate: number) =>
i18n.translate('xpack.uptime.alerts.tls.validAfterExpiringString', {
defaultMessage: `expires on {date} in {relativeDate} days.`,
values: {
date,
relativeDate,
},
}),
validBeforeExpiredString: (date: string, relativeDate: number) =>
i18n.translate('xpack.uptime.alerts.tls.validBeforeExpiredString', {
defaultMessage: 'valid since {date}, {relativeDate} days ago.',
values: {
date,
relativeDate,
},
}),
validBeforeExpiringString: (date: string, relativeDate: number) =>
i18n.translate('xpack.uptime.alerts.tls.validBeforeExpiringString', {
defaultMessage: 'invalid until {date}, {relativeDate} days from now.',
values: {
date,
relativeDate,
},
}),
};

View file

@ -20,8 +20,10 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = asyn
index,
from,
to,
search,
size,
search,
notValidBefore,
notValidAfter,
sortBy,
direction,
}) => {
@ -91,6 +93,10 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = asyn
},
};
if (!params.body.query.bool.should) {
params.body.query.bool.should = [];
}
if (search) {
params.body.query.bool.minimum_should_match = 1;
params.body.query.bool.should = [
@ -110,6 +116,35 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = asyn
];
}
if (notValidBefore || notValidAfter) {
const validityFilters: any = {
bool: {
should: [],
},
};
if (notValidBefore) {
validityFilters.bool.should.push({
range: {
'tls.certificate_not_valid_before': {
lte: notValidBefore,
},
},
});
}
if (notValidAfter) {
validityFilters.bool.should.push({
range: {
'tls.certificate_not_valid_after': {
lte: notValidAfter,
},
},
});
}
params.body.query.bool.filter.push(validityFilters);
}
// console.log(JSON.stringify(params, null, 2));
const result = await callES('search', params);
const certs = (result?.hits?.hits ?? []).map((hit: any) => {

View file

@ -46,6 +46,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./monitor_states_generated'));
loadTestFile(require.resolve('./telemetry_collectors'));
});
describe('with real-world data', () => {
beforeEach('load heartbeat data', async () => await esArchiver.load('uptime/full_heartbeat'));
afterEach('unload', async () => await esArchiver.unload('uptime/full_heartbeat'));

View file

@ -13,6 +13,7 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) {
return {
async openFlyout() {
await testSubjects.click('xpack.uptime.alertsPopover.toggleButton', 5000);
await testSubjects.click('xpack.uptime.openAlertContextPanel', 5000);
await testSubjects.click('xpack.uptime.toggleAlertFlyout', 5000);
},
async openMonitorStatusAlertType(alertType: string) {