Skip to content

Commit

Permalink
Onboard Synthetics TLS rule type with FAAD (#191127)
Browse files Browse the repository at this point in the history
Resolves: #169867

This is the second attempt PR 🙂 to onboard the Synthetics TLS rule type
with FAAD.

**To verify**
1. Create an oblt cluster with `/create-ccs-cluster` on slack. Choose
`dev-oblt`.
2. Add the configuration values from the oblt command to your
kibana.yml. You may have to add:
```
elasticsearch.ignoreVersionMismatch: true
```
and start Kibana
4. Navigate to `app/synthetics/settings/alerting` and add a default
connector.
5. Go to `/app/synthetics/monitors/getting-started` and create a HTTP
Ping monitor with whatever url you want ( I used https://github.com/)
and select a location.
6. Go back to `app/synthetics` and click the Alerts & Rules link. Click
TLS certificate rule. Edit the older than param to something low, such
as 1 day.
7. The TLS rule should create an active alert, verify that the action
message is populated.
8. Repeat step 5 update the older than param to be higher than the age
of the cert. You can check your cert here `app/synthetics/certificates`
9. The TLS rule should recover, and verify that the recovery action
message is populated.
10. You can also check the AAD docs in dev tools using the following
command:
```
GET .internal.alerts-observability.uptime.alerts*/_search
```
  • Loading branch information
doakalexi authored Aug 27, 2024
1 parent fd312e3 commit 51e76d8
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,9 @@ export const syntheticsRuleTypeFieldMap = {
...legacyExperimentalFieldMap,
};

export const SyntheticsRuleTypeAlertDefinition: IRuleTypeAlerts = {
export const SyntheticsRuleTypeAlertDefinition: IRuleTypeAlerts<ObservabilityUptimeAlert> = {
context: SYNTHETICS_RULE_TYPES_ALERT_CONTEXT,
mappings: { fieldMap: syntheticsRuleTypeFieldMap },
useLegacyAlerts: true,
shouldWrite: true,
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
import { isEmpty } from 'lodash';
import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common';
import { PluginSetupContract } from '@kbn/alerting-plugin/server';
import {
GetViewInAppRelativeUrlFnOpts,
AlertInstanceContext as AlertContext,
RuleExecutorOptions,
AlertsClientError,
IRuleTypeAlerts,
} from '@kbn/alerting-plugin/server';
import { observabilityPaths } from '@kbn/observability-plugin/common';
import { ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils';
Expand Down Expand Up @@ -55,16 +53,15 @@ type MonitorStatusAlert = ObservabilityUptimeAlert;
export const registerSyntheticsStatusCheckRule = (
server: SyntheticsServerSetup,
plugins: SyntheticsPluginsSetupDependencies,
syntheticsMonitorClient: SyntheticsMonitorClient,
alerting: PluginSetupContract
syntheticsMonitorClient: SyntheticsMonitorClient
) => {
if (!alerting) {
if (!plugins.alerting) {
throw new Error(
'Cannot register the synthetics monitor status rule type. The alerting plugin needs to be enabled.'
);
}

alerting.registerType({
plugins.alerting.registerType({
id: SYNTHETICS_ALERT_RULE_TYPES.MONITOR_STATUS,
category: DEFAULT_APP_CATEGORIES.observability.id,
producer: 'uptime',
Expand Down Expand Up @@ -172,10 +169,7 @@ export const registerSyntheticsStatusCheckRule = (
state: updateState(ruleState, !isEmpty(downConfigs), { downConfigs }),
};
},
alerts: {
...SyntheticsRuleTypeAlertDefinition,
shouldWrite: true,
} as IRuleTypeAlerts<MonitorStatusAlert>,
alerts: SyntheticsRuleTypeAlertDefinition,
fieldsForAAD: Object.keys(syntheticsRuleFieldMap),
getViewInAppRelativeUrl: ({ rule }: GetViewInAppRelativeUrlFnOpts<{}>) =>
observabilityPaths.ruleDetails(rule.id),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* 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 { IBasePath } from '@kbn/core/server';
import { AlertsLocatorParams } from '@kbn/observability-plugin/common';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { setTLSRecoveredAlertsContext } from './message_utils';
import { TLSLatestPing } from './tls_rule_executor';

describe('setTLSRecoveredAlertsContext', () => {
const timestamp = new Date().toISOString();
const alertUuid = 'alert-id';
const configId = '12345';
const basePath = {
publicBaseUrl: 'https://localhost:5601',
} as IBasePath;
const alertsLocatorMock = {
getLocation: jest.fn().mockImplementation(() => ({
path: 'https://localhost:5601/app/observability/alerts/alert-id',
})),
} as any as LocatorPublic<AlertsLocatorParams>;
const alertState = {
summary: 'test-summary',
status: 'has expired',
sha256: 'cert-1-sha256',
commonName: 'cert-1',
issuer: 'test-issuer',
monitorName: 'test-monitor',
monitorType: 'test-monitor-type',
locationName: 'test-location-name',
monitorUrl: 'test-monitor-url',
configId,
};

it('sets context correctly when monitor cert has been updated', async () => {
const alertsClientMock = {
report: jest.fn(),
getAlertLimitValue: jest.fn().mockReturnValue(10),
setAlertLimitReached: jest.fn(),
getRecoveredAlerts: jest.fn().mockReturnValue([
{
alert: {
getId: () => alertUuid,
getState: () => alertState,
setContext: jest.fn(),
getUuid: () => alertUuid,
getStart: () => new Date().toISOString(),
},
},
]),
setAlertData: jest.fn(),
isTrackedAlert: jest.fn(),
};
await setTLSRecoveredAlertsContext({
alertsClient: alertsClientMock,
basePath,
defaultStartedAt: timestamp,
spaceId: 'default',
alertsLocator: alertsLocatorMock,
latestPings: [
{
config_id: configId,
'@timestamp': timestamp,
tls: {
server: {
hash: {
sha256: 'cert-2-sha256',
},
x509: {
subject: {
common_name: 'cert-2',
},
not_after: timestamp,
},
},
},
} as TLSLatestPing,
],
});
expect(alertsClientMock.setAlertData).toBeCalledWith({
context: {
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
commonName: 'cert-1',
configId: '12345',
issuer: 'test-issuer',
locationName: 'test-location-name',
monitorName: 'test-monitor',
monitorType: 'test-monitor-type',
monitorUrl: 'test-monitor-url',
newStatus: expect.stringContaining('Certificate cert-2 Expired on'),
previousStatus: 'Certificate cert-1 test-summary',
sha256: 'cert-1-sha256',
status: 'has expired',
summary: 'Monitor certificate has been updated.',
},
id: 'alert-id',
});
});

it('sets context correctly when monitor cert expiry/age threshold has been updated', async () => {
const alertsClientMock = {
report: jest.fn(),
getAlertLimitValue: jest.fn().mockReturnValue(10),
setAlertLimitReached: jest.fn(),
getRecoveredAlerts: jest.fn().mockReturnValue([
{
alert: {
getId: () => alertUuid,
getState: () => alertState,
setContext: jest.fn(),
getUuid: () => alertUuid,
getStart: () => new Date().toISOString(),
},
},
]),
setAlertData: jest.fn(),
isTrackedAlert: jest.fn(),
};
await setTLSRecoveredAlertsContext({
alertsClient: alertsClientMock,
basePath,
defaultStartedAt: timestamp,
spaceId: 'default',
alertsLocator: alertsLocatorMock,
latestPings: [
{
config_id: configId,
'@timestamp': timestamp,
tls: {
server: {
hash: {
sha256: 'cert-1-sha256',
},
x509: {
subject: {
common_name: 'cert-1',
},
not_after: timestamp,
},
},
},
} as TLSLatestPing,
],
});
expect(alertsClientMock.setAlertData).toBeCalledWith({
context: {
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
commonName: 'cert-1',
configId: '12345',
issuer: 'test-issuer',
locationName: 'test-location-name',
monitorName: 'test-monitor',
monitorType: 'test-monitor-type',
monitorUrl: 'test-monitor-url',
newStatus: 'Certificate cert-1 test-summary',
previousStatus: 'Certificate cert-1 test-summary',
sha256: 'cert-1-sha256',
status: 'has expired',
summary: 'Expiry/Age threshold has been updated.',
},
id: 'alert-id',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ import moment from 'moment/moment';
import { IBasePath } from '@kbn/core-http-server';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { AlertsLocatorParams, getAlertUrl } from '@kbn/observability-plugin/common';
import { RuleExecutorServices } from '@kbn/alerting-plugin/server';
import {
AlertInstanceContext as AlertContext,
AlertInstanceState as AlertState,
ActionGroupIdsOf,
} from '@kbn/alerting-plugin/server';
import { i18n } from '@kbn/i18n';
import { PublicAlertsClient } from '@kbn/alerting-plugin/server/alerts_client/types';
import { ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils';
import { TLSLatestPing } from './tls_rule_executor';
import { ALERT_DETAILS_URL } from '../action_variables';
import { Cert } from '../../../common/runtime_types';
import { tlsTranslations } from '../translations';
import { MonitorStatusActionGroup } from '../../../common/constants/synthetics_alerts';
interface TLSContent {
summary: string;
status?: string;
Expand Down Expand Up @@ -75,35 +82,34 @@ export const getCertSummary = (cert: Cert, expirationThreshold: number, ageThres
};
};

type CertSummary = ReturnType<typeof getCertSummary>;

export const setTLSRecoveredAlertsContext = async ({
alertFactory,
alertsClient,
basePath,
defaultStartedAt,
getAlertStartedDate,
spaceId,
alertsLocator,
getAlertUuid,
latestPings,
}: {
alertFactory: RuleExecutorServices['alertFactory'];
alertsClient: PublicAlertsClient<
ObservabilityUptimeAlert,
AlertState,
AlertContext,
ActionGroupIdsOf<MonitorStatusActionGroup>
>;
defaultStartedAt: string;
getAlertStartedDate: (alertInstanceId: string) => string | null;
basePath: IBasePath;
spaceId: string;
alertsLocator?: LocatorPublic<AlertsLocatorParams>;
getAlertUuid?: (alertId: string) => string | null;
latestPings: TLSLatestPing[];
}) => {
const { getRecoveredAlerts } = alertFactory.done();
const recoveredAlerts = alertsClient.getRecoveredAlerts() ?? [];

for await (const alert of getRecoveredAlerts()) {
const recoveredAlertId = alert.getId();
const alertUuid = getAlertUuid?.(recoveredAlertId) || null;
const indexedStartedAt = getAlertStartedDate(recoveredAlertId) ?? defaultStartedAt;
for (const recoveredAlert of recoveredAlerts) {
const recoveredAlertId = recoveredAlert.alert.getId();
const alertUuid = recoveredAlert.alert.getUuid();
const indexedStartedAt = recoveredAlert.alert.getStart() ?? defaultStartedAt;

const state = alert.getState() as CertSummary;
const state = recoveredAlert.alert.getState();
const alertUrl = await getAlertUrl(
alertUuid,
spaceId,
Expand Down Expand Up @@ -144,12 +150,13 @@ export const setTLSRecoveredAlertsContext = async ({
newStatus = previousStatus;
}

alert.setContext({
const context = {
...state,
newStatus,
previousStatus,
summary: newSummary,
[ALERT_DETAILS_URL]: alertUrl,
});
};
alertsClient.setAlertData({ id: recoveredAlertId, context });
}
};
Loading

0 comments on commit 51e76d8

Please sign in to comment.