[Uptime] Certificates page (#64059)
This commit is contained in:
parent
c8ddb6b8ed
commit
523926f0a4
|
@ -10,6 +10,8 @@ export const OVERVIEW_ROUTE = '/';
|
|||
|
||||
export const SETTINGS_ROUTE = '/settings';
|
||||
|
||||
export const CERTIFICATES_ROUTE = '/certificates';
|
||||
|
||||
export enum STATUS {
|
||||
UP = 'up',
|
||||
DOWN = 'down',
|
||||
|
@ -41,3 +43,10 @@ export const SHORT_TIMESPAN_LOCALE = {
|
|||
yy: '%d Yr',
|
||||
},
|
||||
};
|
||||
|
||||
export enum CERT_STATUS {
|
||||
OK = 'OK',
|
||||
EXPIRING_SOON = 'EXPIRING_SOON',
|
||||
EXPIRED = 'EXPIRED',
|
||||
TOO_OLD = 'TOO_OLD',
|
||||
}
|
||||
|
|
|
@ -8,35 +8,45 @@ import * as t from 'io-ts';
|
|||
|
||||
export const GetCertsParamsType = t.intersection([
|
||||
t.type({
|
||||
from: t.string,
|
||||
to: t.string,
|
||||
index: t.number,
|
||||
size: t.number,
|
||||
sortBy: t.string,
|
||||
direction: t.string,
|
||||
}),
|
||||
t.partial({
|
||||
search: t.string,
|
||||
from: t.string,
|
||||
to: t.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
export type GetCertsParams = t.TypeOf<typeof GetCertsParamsType>;
|
||||
|
||||
export const CertMonitorType = t.partial({
|
||||
name: t.string,
|
||||
id: t.string,
|
||||
url: t.string,
|
||||
});
|
||||
|
||||
export const CertType = t.intersection([
|
||||
t.type({
|
||||
monitors: t.array(
|
||||
t.partial({
|
||||
name: t.string,
|
||||
id: t.string,
|
||||
})
|
||||
),
|
||||
monitors: t.array(CertMonitorType),
|
||||
sha256: t.string,
|
||||
}),
|
||||
t.partial({
|
||||
certificate_not_valid_after: t.string,
|
||||
certificate_not_valid_before: t.string,
|
||||
not_after: t.string,
|
||||
not_before: t.string,
|
||||
common_name: t.string,
|
||||
issuer: t.string,
|
||||
sha1: t.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const CertResultType = t.type({
|
||||
certs: t.array(CertType),
|
||||
total: t.number,
|
||||
});
|
||||
|
||||
export type Cert = t.TypeOf<typeof CertType>;
|
||||
export type CertMonitor = t.TypeOf<typeof CertMonitorType>;
|
||||
export type CertResult = t.TypeOf<typeof CertResultType>;
|
||||
|
|
|
@ -17,8 +17,8 @@ export const HttpResponseBodyType = t.partial({
|
|||
export type HttpResponseBody = t.TypeOf<typeof HttpResponseBodyType>;
|
||||
|
||||
export const TlsType = t.partial({
|
||||
certificate_not_valid_after: t.string,
|
||||
certificate_not_valid_before: t.string,
|
||||
not_after: t.string,
|
||||
not_before: t.string,
|
||||
});
|
||||
|
||||
export type Tls = t.TypeOf<typeof TlsType>;
|
||||
|
|
134
x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_monitors.test.tsx.snap
generated
Normal file
134
x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_monitors.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,134 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CertMonitors renders expected elements for valid props 1`] = `
|
||||
<span>
|
||||
<span>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
class="euiLink euiLink--primary"
|
||||
type="button"
|
||||
>
|
||||
<a
|
||||
data-test-subj="monitor-page-link-bad-ssl-dashboard"
|
||||
href="/monitor/YmFkLXNzbC1kYXNoYm9hcmQ="
|
||||
>
|
||||
bad-ssl-dashboard
|
||||
</a>
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
,
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
class="euiLink euiLink--primary"
|
||||
type="button"
|
||||
>
|
||||
<a
|
||||
data-test-subj="monitor-page-link-elastic-co"
|
||||
href="/monitor/ZWxhc3RpYy1jbw=="
|
||||
>
|
||||
elastic
|
||||
</a>
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
,
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
class="euiLink euiLink--primary"
|
||||
type="button"
|
||||
>
|
||||
<a
|
||||
data-test-subj="monitor-page-link-extended-validation"
|
||||
href="/monitor/ZXh0ZW5kZWQtdmFsaWRhdGlvbg=="
|
||||
>
|
||||
extended-validation
|
||||
</a>
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`CertMonitors shallow renders expected elements for valid props 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"history": Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"canGo": [Function],
|
||||
"createHref": [Function],
|
||||
"entries": Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"index": 0,
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
},
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"match": Object {
|
||||
"isExact": true,
|
||||
"params": Object {},
|
||||
"path": "/",
|
||||
"url": "/",
|
||||
},
|
||||
"staticContext": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CertMonitors
|
||||
monitors={
|
||||
Array [
|
||||
Object {
|
||||
"id": "bad-ssl-dashboard",
|
||||
"name": "",
|
||||
"url": "https://badssl.com/dashboard/",
|
||||
},
|
||||
Object {
|
||||
"id": "elastic-co",
|
||||
"name": "elastic",
|
||||
"url": "https://www.elastic.co/",
|
||||
},
|
||||
Object {
|
||||
"id": "extended-validation",
|
||||
"name": "",
|
||||
"url": "https://extended-validation.badssl.com/",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
|
@ -0,0 +1,93 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CertificatesSearch renders expected elements for valid props 1`] = `
|
||||
.c0 {
|
||||
min-width: 700px;
|
||||
}
|
||||
|
||||
<div
|
||||
class="euiFormControlLayout"
|
||||
>
|
||||
<div
|
||||
class="euiFormControlLayout__childrenWrapper"
|
||||
>
|
||||
<input
|
||||
aria-label="Search certificates"
|
||||
class="euiFieldSearch c0"
|
||||
data-test-subj="uptimeCertSearch"
|
||||
placeholder="Search certificates"
|
||||
type="search"
|
||||
/>
|
||||
<div
|
||||
class="euiFormControlLayoutIcons"
|
||||
>
|
||||
<span
|
||||
class="euiFormControlLayoutCustomIcon"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="euiFormControlLayoutCustomIcon__icon"
|
||||
data-euiicon-type="search"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CertificatesSearch shallow renders expected elements for valid props 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"history": Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"canGo": [Function],
|
||||
"createHref": [Function],
|
||||
"entries": Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"index": 0,
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
},
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"match": Object {
|
||||
"isExact": true,
|
||||
"params": Object {},
|
||||
"path": "/",
|
||||
"url": "/",
|
||||
},
|
||||
"staticContext": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CertificateSearch
|
||||
setSearch={[MockFunction]}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
100
x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_status.test.tsx.snap
generated
Normal file
100
x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_status.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,100 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CertStatus renders expected elements for valid props 1`] = `
|
||||
<div
|
||||
class="euiHealth"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div
|
||||
color="success"
|
||||
data-euiicon-type="dot"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<span>
|
||||
OK
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CertStatus shallow renders expected elements for valid props 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"history": Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"canGo": [Function],
|
||||
"createHref": [Function],
|
||||
"entries": Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"index": 0,
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
},
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"match": Object {
|
||||
"isExact": true,
|
||||
"params": Object {},
|
||||
"path": "/",
|
||||
"url": "/",
|
||||
},
|
||||
"staticContext": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CertStatus
|
||||
cert={
|
||||
Object {
|
||||
"common_name": "github.com",
|
||||
"issuer": "DigiCert SHA2 Extended Validation Server CA",
|
||||
"monitors": Array [
|
||||
Object {
|
||||
"id": "github",
|
||||
"name": "",
|
||||
"url": "https://github.com/",
|
||||
},
|
||||
],
|
||||
"not_after": "2020-05-08T00:00:00.000Z",
|
||||
"not_before": "2018-05-08T00:00:00.000Z",
|
||||
"sha1": "ca06f56b258b7a0d4f2b05470939478651151984",
|
||||
"sha256": "3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
|
@ -0,0 +1,70 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CertificateList shallow renders expected elements for valid props 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"history": Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"canGo": [Function],
|
||||
"createHref": [Function],
|
||||
"entries": Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"index": 0,
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
},
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"match": Object {
|
||||
"isExact": true,
|
||||
"params": Object {},
|
||||
"path": "/",
|
||||
"url": "/",
|
||||
},
|
||||
"staticContext": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CertificateList
|
||||
onChange={[MockFunction]}
|
||||
page={
|
||||
Object {
|
||||
"index": 0,
|
||||
"size": 10,
|
||||
}
|
||||
}
|
||||
sort={
|
||||
Object {
|
||||
"direction": "asc",
|
||||
"field": "not_after",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
|
@ -0,0 +1,171 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FingerprintCol renders expected elements for valid props 1`] = `
|
||||
Array [
|
||||
.c1 .euiButtonEmpty__content {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
<span
|
||||
class="c0"
|
||||
data-test-subj="CA06F56B258B7A0D4F2B05470939478651151984"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--primary c1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
SHA 1
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary"
|
||||
title="Click to copy fingerprint value"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
data-euiicon-type="copy"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</span>,
|
||||
.c1 .euiButtonEmpty__content {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
<span
|
||||
class="c0"
|
||||
data-test-subj="3111500C4A66012CDAE333EC3FCA1C9DDE45C954440E7EE413716BFF3663C074"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
class="euiButtonEmpty euiButtonEmpty--primary c1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonEmpty__text"
|
||||
>
|
||||
SHA 256
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
>
|
||||
<button
|
||||
class="euiButtonIcon euiButtonIcon--primary"
|
||||
title="Click to copy fingerprint value"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
data-euiicon-type="copy"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</span>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`FingerprintCol shallow renders expected elements for valid props 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"history": Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"canGo": [Function],
|
||||
"createHref": [Function],
|
||||
"entries": Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"index": 0,
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
},
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"match": Object {
|
||||
"isExact": true,
|
||||
"params": Object {},
|
||||
"path": "/",
|
||||
"url": "/",
|
||||
},
|
||||
"staticContext": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FingerprintCol
|
||||
cert={
|
||||
Object {
|
||||
"common_name": "github.com",
|
||||
"issuer": "DigiCert SHA2 Extended Validation Server CA",
|
||||
"monitors": Array [
|
||||
Object {
|
||||
"id": "github",
|
||||
"name": "",
|
||||
"url": "https://github.com/",
|
||||
},
|
||||
],
|
||||
"not_after": "2020-05-08T00:00:00.000Z",
|
||||
"not_before": "2018-05-08T00:00:00.000Z",
|
||||
"sha1": "ca06f56b258b7a0d4f2b05470939478651151984",
|
||||
"sha256": "3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { CertMonitors } from '../cert_monitors';
|
||||
import { renderWithRouter, shallowWithRouter } from '../../../lib';
|
||||
|
||||
describe('CertMonitors', () => {
|
||||
const certMons = [
|
||||
{ name: '', id: 'bad-ssl-dashboard', url: 'https://badssl.com/dashboard/' },
|
||||
{ name: 'elastic', id: 'elastic-co', url: 'https://www.elastic.co/' },
|
||||
{ name: '', id: 'extended-validation', url: 'https://extended-validation.badssl.com/' },
|
||||
];
|
||||
it('shallow renders expected elements for valid props', () => {
|
||||
expect(shallowWithRouter(<CertMonitors monitors={certMons} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders expected elements for valid props', () => {
|
||||
expect(renderWithRouter(<CertMonitors monitors={certMons} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { renderWithRouter, shallowWithRouter } from '../../../lib';
|
||||
import { CertificateSearch } from '../cert_search';
|
||||
|
||||
describe('CertificatesSearch', () => {
|
||||
it('shallow renders expected elements for valid props', () => {
|
||||
expect(shallowWithRouter(<CertificateSearch setSearch={jest.fn()} />)).toMatchSnapshot();
|
||||
});
|
||||
it('renders expected elements for valid props', () => {
|
||||
expect(renderWithRouter(<CertificateSearch setSearch={jest.fn()} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { renderWithRouter, shallowWithRouter } from '../../../lib';
|
||||
import { CertStatus } from '../cert_status';
|
||||
import * as redux from 'react-redux';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('CertStatus', () => {
|
||||
beforeEach(() => {
|
||||
const spy = jest.spyOn(redux, 'useDispatch');
|
||||
spy.mockReturnValue(jest.fn());
|
||||
|
||||
const spy1 = jest.spyOn(redux, 'useSelector');
|
||||
spy1.mockReturnValue(true);
|
||||
});
|
||||
|
||||
const cert = {
|
||||
monitors: [{ name: '', id: 'github', url: 'https://github.com/' }],
|
||||
not_after: '2020-05-08T00:00:00.000Z',
|
||||
not_before: '2018-05-08T00:00:00.000Z',
|
||||
issuer: 'DigiCert SHA2 Extended Validation Server CA',
|
||||
sha1: 'ca06f56b258b7a0d4f2b05470939478651151984',
|
||||
sha256: '3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074',
|
||||
common_name: 'github.com',
|
||||
};
|
||||
|
||||
it('shallow renders expected elements for valid props', () => {
|
||||
expect(shallowWithRouter(<CertStatus cert={cert} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders expected elements for valid props', () => {
|
||||
cert.not_after = moment()
|
||||
.add('4', 'months')
|
||||
.toISOString();
|
||||
expect(renderWithRouter(<CertStatus cert={cert} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -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 React from 'react';
|
||||
import { shallowWithRouter } from '../../../lib';
|
||||
import { CertificateList, CertSort } from '../certificates_list';
|
||||
|
||||
describe('CertificateList', () => {
|
||||
it('shallow renders expected elements for valid props', () => {
|
||||
const page = {
|
||||
index: 0,
|
||||
size: 10,
|
||||
};
|
||||
const sort: CertSort = {
|
||||
field: 'not_after',
|
||||
direction: 'asc',
|
||||
};
|
||||
|
||||
expect(
|
||||
shallowWithRouter(<CertificateList page={page} sort={sort} onChange={jest.fn()} />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { renderWithRouter, shallowWithRouter } from '../../../lib';
|
||||
import { FingerprintCol } from '../fingerprint_col';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('FingerprintCol', () => {
|
||||
const cert = {
|
||||
monitors: [{ name: '', id: 'github', url: 'https://github.com/' }],
|
||||
not_after: '2020-05-08T00:00:00.000Z',
|
||||
not_before: '2018-05-08T00:00:00.000Z',
|
||||
issuer: 'DigiCert SHA2 Extended Validation Server CA',
|
||||
sha1: 'ca06f56b258b7a0d4f2b05470939478651151984',
|
||||
sha256: '3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074',
|
||||
common_name: 'github.com',
|
||||
};
|
||||
|
||||
it('shallow renders expected elements for valid props', () => {
|
||||
expect(shallowWithRouter(<FingerprintCol cert={cert} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders expected elements for valid props', () => {
|
||||
cert.not_after = moment()
|
||||
.add('4', 'months')
|
||||
.toISOString();
|
||||
|
||||
expect(renderWithRouter(<FingerprintCol cert={cert} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { EuiToolTip } from '@elastic/eui';
|
||||
import { CertMonitor } from '../../../common/runtime_types';
|
||||
import { MonitorPageLink } from '../common/monitor_page_link';
|
||||
|
||||
interface Props {
|
||||
monitors: CertMonitor[];
|
||||
}
|
||||
|
||||
export const CertMonitors: React.FC<Props> = ({ monitors }) => {
|
||||
return (
|
||||
<span>
|
||||
{monitors.map((mon: CertMonitor, ind: number) => (
|
||||
<span key={mon.id}>
|
||||
{ind > 0 && ', '}
|
||||
<EuiToolTip content={mon.url}>
|
||||
<MonitorPageLink monitorId={mon.id!} linkParameters={''}>
|
||||
{mon.name || mon.id}
|
||||
</MonitorPageLink>
|
||||
</EuiToolTip>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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, { ChangeEvent } from 'react';
|
||||
import { EuiFieldSearch } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import * as labels from './translations';
|
||||
|
||||
const WrapFieldSearch = styled(EuiFieldSearch)`
|
||||
min-width: 700px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
setSearch: (val: string) => void;
|
||||
}
|
||||
|
||||
export const CertificateSearch: React.FC<Props> = ({ setSearch }) => {
|
||||
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<WrapFieldSearch
|
||||
data-test-subj="uptimeCertSearch"
|
||||
placeholder={labels.SEARCH_CERTS}
|
||||
onChange={onChange}
|
||||
isClearable={true}
|
||||
aria-label={labels.SEARCH_CERTS}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { EuiHealth } from '@elastic/eui';
|
||||
import { Cert } from '../../../common/runtime_types';
|
||||
import { useCertStatus } from '../../hooks';
|
||||
import * as labels from './translations';
|
||||
import { CERT_STATUS } from '../../../common/constants';
|
||||
|
||||
interface Props {
|
||||
cert: Cert;
|
||||
}
|
||||
|
||||
export const CertStatus: React.FC<Props> = ({ cert }) => {
|
||||
const certStatus = useCertStatus(cert?.not_after, cert?.not_before);
|
||||
|
||||
if (certStatus === CERT_STATUS.EXPIRING_SOON) {
|
||||
return (
|
||||
<EuiHealth color="warning">
|
||||
<span>{labels.EXPIRES_SOON}</span>
|
||||
</EuiHealth>
|
||||
);
|
||||
}
|
||||
if (certStatus === CERT_STATUS.EXPIRED) {
|
||||
return (
|
||||
<EuiHealth color="danger">
|
||||
<span>{labels.EXPIRED}</span>
|
||||
</EuiHealth>
|
||||
);
|
||||
}
|
||||
|
||||
if (certStatus === CERT_STATUS.TOO_OLD) {
|
||||
return (
|
||||
<EuiHealth color="danger">
|
||||
<span>{labels.TOO_OLD}</span>
|
||||
</EuiHealth>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiHealth color="success">
|
||||
<span>{labels.OK}</span>
|
||||
</EuiHealth>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Direction, EuiBasicTable } from '@elastic/eui';
|
||||
import { certificatesSelector } from '../../state/certificates/certificates';
|
||||
import { CertStatus } from './cert_status';
|
||||
import { CertMonitors } from './cert_monitors';
|
||||
import * as labels from './translations';
|
||||
import { Cert, CertMonitor } from '../../../common/runtime_types';
|
||||
import { FingerprintCol } from './fingerprint_col';
|
||||
|
||||
interface Page {
|
||||
index: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export type CertFields =
|
||||
| 'sha256'
|
||||
| 'sha1'
|
||||
| 'issuer'
|
||||
| 'common_name'
|
||||
| 'monitors'
|
||||
| 'not_after'
|
||||
| 'not_before';
|
||||
|
||||
export interface CertSort {
|
||||
field: CertFields;
|
||||
direction: Direction;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
page: Page;
|
||||
sort: CertSort;
|
||||
onChange: (page: Page, sort: CertSort) => void;
|
||||
}
|
||||
|
||||
export const CertificateList: React.FC<Props> = ({ page, sort, onChange }) => {
|
||||
const certificates = useSelector(certificatesSelector);
|
||||
|
||||
const onTableChange = (newVal: Partial<Props>) => {
|
||||
onChange(newVal.page as Page, newVal.sort as CertSort);
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
pageIndex: page.index,
|
||||
pageSize: page.size,
|
||||
totalItemCount: certificates?.total ?? 0,
|
||||
pageSizeOptions: [10, 25, 50, 100],
|
||||
hidePerPageOptions: false,
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'not_after',
|
||||
name: labels.STATUS_COL,
|
||||
sortable: true,
|
||||
render: (val: string, item: Cert) => <CertStatus cert={item} />,
|
||||
},
|
||||
{
|
||||
name: labels.COMMON_NAME_COL,
|
||||
field: 'common_name',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: labels.MONITORS_COL,
|
||||
field: 'monitors',
|
||||
render: (monitors: CertMonitor[]) => <CertMonitors monitors={monitors} />,
|
||||
},
|
||||
{
|
||||
name: labels.ISSUED_BY_COL,
|
||||
field: 'issuer',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: labels.VALID_UNTIL_COL,
|
||||
field: 'not_after',
|
||||
sortable: true,
|
||||
render: (value: string) => moment(value).format('L LT'),
|
||||
},
|
||||
{
|
||||
name: labels.AGE_COL,
|
||||
field: 'not_before',
|
||||
sortable: true,
|
||||
render: (value: string) => moment().diff(moment(value), 'days') + ' ' + labels.DAYS,
|
||||
},
|
||||
{
|
||||
name: labels.FINGERPRINTS_COL,
|
||||
field: 'sha256',
|
||||
render: (val: string, item: Cert) => <FingerprintCol cert={item} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
columns={columns}
|
||||
items={certificates?.certs ?? []}
|
||||
pagination={pagination}
|
||||
onChange={onTableChange}
|
||||
sorting={{
|
||||
sort: {
|
||||
field: sort.field,
|
||||
direction: sort.direction,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiButtonIcon, EuiCopy, EuiToolTip } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { Cert } from '../../../common/runtime_types';
|
||||
import { COPY_FINGERPRINT } from './translations';
|
||||
|
||||
const EmptyButton = styled(EuiButtonEmpty)`
|
||||
.euiButtonEmpty__content {
|
||||
padding-right: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Span = styled.span`
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
cert: Cert;
|
||||
}
|
||||
|
||||
export const FingerprintCol: React.FC<Props> = ({ cert }) => {
|
||||
const ShaComponent = ({ text, val }: { text: string; val: string }) => {
|
||||
return (
|
||||
<Span data-test-subj={val}>
|
||||
<EuiToolTip content={val}>
|
||||
<EmptyButton>{text} </EmptyButton>
|
||||
</EuiToolTip>
|
||||
<EuiCopy textToCopy={val ?? ''}>
|
||||
{copy => <EuiButtonIcon onClick={copy} iconType="copy" title={COPY_FINGERPRINT} />}
|
||||
</EuiCopy>
|
||||
</Span>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<ShaComponent text="SHA 1" val={cert?.sha1?.toUpperCase() ?? ''} />
|
||||
<ShaComponent text="SHA 256" val={cert?.sha256?.toUpperCase() ?? ''} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './cert_monitors';
|
||||
export * from './cert_search';
|
||||
export * from './cert_status';
|
||||
export * from './certificates_list';
|
||||
export * from './fingerprint_col';
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 OK = i18n.translate('xpack.uptime.certs.ok', {
|
||||
defaultMessage: 'OK',
|
||||
});
|
||||
|
||||
export const EXPIRED = i18n.translate('xpack.uptime.certs.expired', {
|
||||
defaultMessage: 'Expired',
|
||||
});
|
||||
|
||||
export const EXPIRES_SOON = i18n.translate('xpack.uptime.certs.expireSoon', {
|
||||
defaultMessage: 'Expires soon',
|
||||
});
|
||||
|
||||
export const SEARCH_CERTS = i18n.translate('xpack.uptime.certs.searchCerts', {
|
||||
defaultMessage: 'Search certificates',
|
||||
});
|
||||
|
||||
export const STATUS_COL = i18n.translate('xpack.uptime.certs.list.status', {
|
||||
defaultMessage: 'Status',
|
||||
});
|
||||
|
||||
export const TOO_OLD = i18n.translate('xpack.uptime.certs.list.status.old', {
|
||||
defaultMessage: 'Too old',
|
||||
});
|
||||
|
||||
export const COMMON_NAME_COL = i18n.translate('xpack.uptime.certs.list.commonName', {
|
||||
defaultMessage: 'Common name',
|
||||
});
|
||||
|
||||
export const MONITORS_COL = i18n.translate('xpack.uptime.certs.list.monitors', {
|
||||
defaultMessage: 'Monitors',
|
||||
});
|
||||
|
||||
export const ISSUED_BY_COL = i18n.translate('xpack.uptime.certs.list.issuedBy', {
|
||||
defaultMessage: 'Issued by',
|
||||
});
|
||||
|
||||
export const VALID_UNTIL_COL = i18n.translate('xpack.uptime.certs.list.validUntil', {
|
||||
defaultMessage: 'Valid until',
|
||||
});
|
||||
|
||||
export const AGE_COL = i18n.translate('xpack.uptime.certs.list.ageCol', {
|
||||
defaultMessage: 'Age',
|
||||
});
|
||||
|
||||
export const DAYS = i18n.translate('xpack.uptime.certs.list.days', {
|
||||
defaultMessage: 'days',
|
||||
});
|
||||
|
||||
export const FINGERPRINTS_COL = i18n.translate('xpack.uptime.certs.list.expirationDate', {
|
||||
defaultMessage: 'Fingerprints',
|
||||
});
|
||||
|
||||
export const COPY_FINGERPRINT = i18n.translate('xpack.uptime.certs.list.copyFingerprint', {
|
||||
defaultMessage: 'Click to copy fingerprint value',
|
||||
});
|
|
@ -4,8 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { MonitorPageLink } from '../monitor_page_link';
|
||||
|
||||
describe('MonitorPageLink component', () => {
|
|
@ -4,9 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
interface DetailPageLinkProps {
|
||||
/**
|
||||
|
@ -19,7 +19,7 @@ interface DetailPageLinkProps {
|
|||
linkParameters: string | undefined;
|
||||
}
|
||||
|
||||
export const MonitorPageLink: FunctionComponent<DetailPageLinkProps> = ({
|
||||
export const MonitorPageLink: FC<DetailPageLinkProps> = ({
|
||||
children,
|
||||
monitorId,
|
||||
linkParameters,
|
|
@ -1,31 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MonitorStatusBar component renders 1`] = `
|
||||
Array [
|
||||
<div
|
||||
class="euiSpacer euiSpacer--s"
|
||||
/>,
|
||||
<div
|
||||
aria-label="SSL certificate expires in 2 months"
|
||||
class="euiText euiText--small euiText--constrainedWidth"
|
||||
>
|
||||
SSL certificate expires
|
||||
<span
|
||||
class="euiBadge euiBadge--iconLeft"
|
||||
style="background-color:#d3dae6;color:#000"
|
||||
>
|
||||
<span
|
||||
class="euiBadge__content"
|
||||
>
|
||||
<span
|
||||
class="euiBadge__text"
|
||||
>
|
||||
in 2 months
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`MonitorStatusBar component renders null if invalid date 1`] = `null`;
|
|
@ -0,0 +1,119 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SSL Certificate component renders 1`] = `
|
||||
Array [
|
||||
<div
|
||||
class="euiText euiText--medium"
|
||||
>
|
||||
Certificate
|
||||
</div>,
|
||||
<div
|
||||
class="euiSpacer euiSpacer--s"
|
||||
/>,
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div
|
||||
aria-label="Expires in 2 months"
|
||||
class="euiText euiText--small eui-displayInline euiText--constrainedWidth"
|
||||
>
|
||||
Expires
|
||||
<span
|
||||
class="euiBadge euiBadge--iconLeft"
|
||||
style="background-color:#d3dae6;color:#000"
|
||||
>
|
||||
<span
|
||||
class="euiBadge__content"
|
||||
>
|
||||
<span
|
||||
class="euiBadge__text"
|
||||
>
|
||||
in 2 months
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem"
|
||||
>
|
||||
<a
|
||||
class="eui-displayInline"
|
||||
href="/certificates"
|
||||
>
|
||||
<div
|
||||
class="euiText euiText--medium"
|
||||
>
|
||||
Certificate overview
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`SSL Certificate component renders null if invalid date 1`] = `null`;
|
||||
|
||||
exports[`SSL Certificate component shallow renders 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"history": Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"canGo": [Function],
|
||||
"createHref": [Function],
|
||||
"entries": Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"index": 0,
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
},
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"match": Object {
|
||||
"isExact": true,
|
||||
"params": Object {},
|
||||
"path": "/",
|
||||
"url": "/",
|
||||
},
|
||||
"staticContext": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<MonitorSSLCertificate
|
||||
tls={
|
||||
Object {
|
||||
"not_after": "2020-04-24T11:41:38.200Z",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
import { renderWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { MonitorStatusBarComponent } from '../monitor_status_bar';
|
||||
import { Ping } from '../../../../../common/runtime_types';
|
||||
import * as redux from 'react-redux';
|
||||
|
||||
describe('MonitorStatusBar component', () => {
|
||||
let monitorStatus: Ping;
|
||||
|
@ -46,6 +47,12 @@ describe('MonitorStatusBar component', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
const spy = jest.spyOn(redux, 'useDispatch');
|
||||
spy.mockReturnValue(jest.fn());
|
||||
|
||||
const spy1 = jest.spyOn(redux, 'useSelector');
|
||||
spy1.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('renders duration in ms, not us', () => {
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
import { renderWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { Tls } from '../../../../../common/runtime_types';
|
||||
import { MonitorSSLCertificate } from '../monitor_status_bar';
|
||||
import * as redux from 'react-redux';
|
||||
import { mountWithRouter, renderWithRouter, shallowWithRouter } from '../../../../lib';
|
||||
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../common/constants';
|
||||
|
||||
describe('MonitorStatusBar component', () => {
|
||||
describe('SSL Certificate component', () => {
|
||||
let monitorTls: Tls;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -21,37 +22,52 @@ describe('MonitorStatusBar component', () => {
|
|||
.toString();
|
||||
|
||||
monitorTls = {
|
||||
certificate_not_valid_after: dateInTwoMonths,
|
||||
not_after: dateInTwoMonths,
|
||||
};
|
||||
|
||||
const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
|
||||
useDispatchSpy.mockReturnValue(jest.fn());
|
||||
|
||||
const useSelectorSpy = jest.spyOn(redux, 'useSelector');
|
||||
useSelectorSpy.mockReturnValue({ settings: DYNAMIC_SETTINGS_DEFAULTS });
|
||||
});
|
||||
|
||||
it('shallow renders', () => {
|
||||
const monitorTls1 = {
|
||||
not_after: '2020-04-24T11:41:38.200Z',
|
||||
};
|
||||
const component = shallowWithRouter(<MonitorSSLCertificate tls={monitorTls1} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const component = renderWithIntl(<MonitorSSLCertificate tls={monitorTls} />);
|
||||
const component = renderWithRouter(<MonitorSSLCertificate tls={monitorTls} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders null if invalid date', () => {
|
||||
monitorTls = {
|
||||
certificate_not_valid_after: 'i am so invalid date',
|
||||
not_after: 'i am so invalid date',
|
||||
};
|
||||
const component = renderWithIntl(<MonitorSSLCertificate tls={monitorTls} />);
|
||||
const component = renderWithRouter(<MonitorSSLCertificate tls={monitorTls} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders expiration date with a warning state if ssl expiry date is less than 30 days', () => {
|
||||
const dateIn15Days = moment()
|
||||
.add(15, 'day')
|
||||
it('renders expiration date with a warning state if ssl expiry date is less than 5 days', () => {
|
||||
const dateIn5Days = moment()
|
||||
.add(5, 'day')
|
||||
.toString();
|
||||
monitorTls = {
|
||||
certificate_not_valid_after: dateIn15Days,
|
||||
not_after: dateIn5Days,
|
||||
};
|
||||
const component = mountWithIntl(<MonitorSSLCertificate tls={monitorTls} />);
|
||||
const component = mountWithRouter(<MonitorSSLCertificate tls={monitorTls} />);
|
||||
|
||||
const badgeComponent = component.find(EuiBadge);
|
||||
|
||||
expect(badgeComponent.props().color).toBe('warning');
|
||||
|
||||
const badgeComponentText = component.find('.euiBadge__text');
|
||||
expect(badgeComponentText.text()).toBe(moment(dateIn15Days).fromNow());
|
||||
expect(badgeComponentText.text()).toBe(moment(dateIn5Days).fromNow());
|
||||
|
||||
expect(badgeComponent.find('span.euiBadge--warning')).toBeTruthy();
|
||||
});
|
||||
|
@ -61,9 +77,9 @@ describe('MonitorStatusBar component', () => {
|
|||
.add(40, 'day')
|
||||
.toString();
|
||||
monitorTls = {
|
||||
certificate_not_valid_after: dateIn40Days,
|
||||
not_after: dateIn40Days,
|
||||
};
|
||||
const component = mountWithIntl(<MonitorSSLCertificate tls={monitorTls} />);
|
||||
const component = mountWithRouter(<MonitorSSLCertificate tls={monitorTls} />);
|
||||
|
||||
const badgeComponent = component.find(EuiBadge);
|
||||
expect(badgeComponent.props().color).toBe('default');
|
|
@ -6,10 +6,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { EuiSpacer, EuiText, EuiBadge } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { EuiSpacer, EuiText, EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Tls } from '../../../../../common/runtime_types';
|
||||
import { useCertStatus } from '../../../../hooks';
|
||||
import { CERT_STATUS, CERTIFICATES_ROUTE } from '../../../../../common/constants';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
|
@ -19,40 +22,79 @@ interface Props {
|
|||
}
|
||||
|
||||
export const MonitorSSLCertificate = ({ tls }: Props) => {
|
||||
const certValidityDate = new Date(tls?.certificate_not_valid_after ?? '');
|
||||
const certStatus = useCertStatus(tls?.not_after);
|
||||
|
||||
const isValidDate = !isNaN(certValidityDate.valueOf());
|
||||
const isExpiringSoon = certStatus === CERT_STATUS.EXPIRING_SOON;
|
||||
|
||||
const dateIn30Days = moment().add('30', 'days');
|
||||
const isExpired = certStatus === CERT_STATUS.EXPIRED;
|
||||
|
||||
const isExpiringInMonth = isValidDate && dateIn30Days > moment(certValidityDate);
|
||||
const relativeDate = moment(tls?.not_after).fromNow();
|
||||
|
||||
return isValidDate ? (
|
||||
return certStatus ? (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText
|
||||
grow={false}
|
||||
size="s"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.uptime.monitorStatusBar.sslCertificateExpiry.label.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'SSL certificate expires {validityDate}',
|
||||
values: { validityDate: moment(certValidityDate).fromNow() },
|
||||
}
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorStatusBar.sslCertificateExpiry.badgeContent"
|
||||
defaultMessage="SSL certificate expires {emphasizedText}"
|
||||
values={{
|
||||
emphasizedText: (
|
||||
<EuiBadge color={isExpiringInMonth ? 'warning' : 'default'}>
|
||||
{moment(certValidityDate).fromNow()}
|
||||
</EuiBadge>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.uptime.monitorStatusBar.sslCertificate.title', {
|
||||
defaultMessage: 'Certificate',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
className="eui-displayInline"
|
||||
grow={false}
|
||||
size="s"
|
||||
aria-label={
|
||||
isExpired
|
||||
? i18n.translate(
|
||||
'xpack.uptime.monitorStatusBar.sslCertificateExpired.label.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Expired {validityDate}',
|
||||
values: { validityDate: relativeDate },
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.uptime.monitorStatusBar.sslCertificateExpiry.label.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Expires {validityDate}',
|
||||
values: { validityDate: relativeDate },
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
{isExpired ? (
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorStatusBar.sslCertificateExpired.badgeContent"
|
||||
defaultMessage="Expired {emphasizedText}"
|
||||
values={{
|
||||
emphasizedText: <EuiBadge color={'danger'}>{relativeDate}</EuiBadge>,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorStatusBar.sslCertificateExpiry.badgeContent"
|
||||
defaultMessage="Expires {emphasizedText}"
|
||||
values={{
|
||||
emphasizedText: (
|
||||
<EuiBadge color={isExpiringSoon ? 'warning' : 'default'}>
|
||||
{relativeDate}
|
||||
</EuiBadge>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Link to={CERTIFICATES_ROUTE} className="eui-displayInline">
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.uptime.monitorStatusBar.sslCertificate.overview', {
|
||||
defaultMessage: 'Certificate overview',
|
||||
})}
|
||||
</EuiText>
|
||||
</Link>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
|
|
@ -556,13 +556,35 @@ exports[`MonitorList component renders the monitor list 1`] = `
|
|||
<div
|
||||
class="euiPanel euiPanel--paddingMedium"
|
||||
>
|
||||
<h5
|
||||
class="euiTitle euiTitle--xsmall"
|
||||
>
|
||||
Monitor status
|
||||
</h5>
|
||||
<div
|
||||
class="euiSpacer euiSpacer--s"
|
||||
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem"
|
||||
>
|
||||
<h5
|
||||
class="euiTitle euiTitle--xsmall"
|
||||
>
|
||||
Monitor status
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<h5
|
||||
class="euiTitle euiTitle--xsmall"
|
||||
>
|
||||
<a
|
||||
data-test-subj="uptimeCertificatesLink"
|
||||
href="/certificates"
|
||||
>
|
||||
View certificates status
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiSpacer euiSpacer--m"
|
||||
/>
|
||||
<div
|
||||
aria-label="Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying 2 items."
|
||||
|
@ -639,9 +661,25 @@ exports[`MonitorList component renders the monitor list 1`] = `
|
|||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="euiTableHeaderCell"
|
||||
data-test-subj="tableHeaderCell_state.tls_3"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--alignCenter"
|
||||
>
|
||||
<span
|
||||
class="euiTableCellContent__text"
|
||||
>
|
||||
TLS Certificate
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="euiTableHeaderCell euiTableHeaderCell--hideForMobile"
|
||||
data-test-subj="tableHeaderCell_histogram.points_3"
|
||||
data-test-subj="tableHeaderCell_histogram.points_4"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
|
@ -657,7 +695,7 @@ exports[`MonitorList component renders the monitor list 1`] = `
|
|||
</th>
|
||||
<td
|
||||
class="euiTableHeaderCell"
|
||||
data-test-subj="tableHeaderCell_monitor_id_4"
|
||||
data-test-subj="tableHeaderCell_monitor_id_5"
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
style="width:24px"
|
||||
|
@ -791,6 +829,22 @@ exports[`MonitorList component renders the monitor list 1`] = `
|
|||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
>
|
||||
TLS Certificate
|
||||
</div>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<span>
|
||||
-
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell euiTableRowCell--hideForMobile"
|
||||
>
|
||||
|
@ -951,6 +1005,22 @@ exports[`MonitorList component renders the monitor list 1`] = `
|
|||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
>
|
||||
TLS Certificate
|
||||
</div>
|
||||
<div
|
||||
class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent"
|
||||
>
|
||||
<span>
|
||||
-
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="euiTableRowCell euiTableRowCell--hideForMobile"
|
||||
>
|
||||
|
|
|
@ -12,12 +12,19 @@ import {
|
|||
} from '../../../../../common/runtime_types';
|
||||
import { MonitorListComponent } from '../monitor_list';
|
||||
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
|
||||
import * as redux from 'react-redux';
|
||||
|
||||
describe('MonitorList component', () => {
|
||||
let result: MonitorSummaryResult;
|
||||
let localStorageMock: any;
|
||||
|
||||
beforeEach(() => {
|
||||
const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
|
||||
useDispatchSpy.mockReturnValue(jest.fn());
|
||||
|
||||
const useSelectorSpy = jest.spyOn(redux, 'useSelector');
|
||||
useSelectorSpy.mockReturnValue(true);
|
||||
|
||||
localStorageMock = {
|
||||
getItem: jest.fn().mockImplementation(() => '25'),
|
||||
setItem: jest.fn(),
|
||||
|
|
|
@ -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 React from 'react';
|
||||
import moment from 'moment';
|
||||
import styled from 'styled-components';
|
||||
import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import { Cert } from '../../../../common/runtime_types';
|
||||
import { useCertStatus } from '../../../hooks';
|
||||
import { EXPIRED, EXPIRES_SOON } from '../../certificates/translations';
|
||||
import { CERT_STATUS } from '../../../../common/constants';
|
||||
|
||||
interface Props {
|
||||
cert: Cert;
|
||||
}
|
||||
|
||||
const Span = styled.span`
|
||||
margin-left: 5px;
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
export const CertStatusColumn: React.FC<Props> = ({ cert }) => {
|
||||
const certStatus = useCertStatus(cert?.not_after);
|
||||
|
||||
const relativeDate = moment(cert?.not_after).fromNow();
|
||||
|
||||
const CertStatus = ({ color, text }: { color: string; text: string }) => {
|
||||
return (
|
||||
<EuiToolTip content={moment(cert?.not_after).format('L LT')}>
|
||||
<EuiText size="s">
|
||||
<EuiIcon color={color} type="lock" size="s" />
|
||||
<Span>
|
||||
{text} {relativeDate}
|
||||
</Span>
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
if (certStatus === CERT_STATUS.EXPIRING_SOON) {
|
||||
return <CertStatus color="warning" text={EXPIRES_SOON} />;
|
||||
}
|
||||
if (certStatus === CERT_STATUS.EXPIRED) {
|
||||
return <CertStatus color="danger" text={EXPIRED} />;
|
||||
}
|
||||
|
||||
return certStatus ? <CertStatus color="success" text={'Expires'} /> : <span>-</span>;
|
||||
};
|
|
@ -18,12 +18,13 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { HistogramPoint, FetchMonitorStatesQueryArgs } from '../../../../common/runtime_types';
|
||||
import { MonitorSummary } from '../../../../common/runtime_types';
|
||||
import { MonitorListStatusColumn } from './monitor_list_status_column';
|
||||
import { ExpandedRowMap } from './types';
|
||||
import { MonitorBarSeries } from '../../common/charts';
|
||||
import { MonitorPageLink } from './monitor_page_link';
|
||||
import { MonitorPageLink } from '../../common/monitor_page_link';
|
||||
import { OverviewPageLink } from './overview_page_link';
|
||||
import * as labels from './translations';
|
||||
import { MonitorListPageSizeSelect } from './monitor_list_page_size_select';
|
||||
|
@ -31,6 +32,8 @@ import { MonitorListDrawer } from './monitor_list_drawer/list_drawer_container';
|
|||
import { MonitorListProps } from './monitor_list_container';
|
||||
import { MonitorList } from '../../../state/reducers/monitor_list';
|
||||
import { useUrlParams } from '../../../hooks';
|
||||
import { CERTIFICATES_ROUTE } from '../../../../common/constants';
|
||||
import { CertStatusColumn } from './cert_status_column';
|
||||
|
||||
interface Props extends MonitorListProps {
|
||||
lastRefresh: number;
|
||||
|
@ -143,6 +146,12 @@ export const MonitorListComponent: React.FC<Props> = ({
|
|||
</TruncatedEuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
align: 'center' as const,
|
||||
field: 'state.tls',
|
||||
name: labels.TLS_COLUMN_LABEL,
|
||||
render: (tls: any) => <CertStatusColumn cert={tls?.[0]} />,
|
||||
},
|
||||
{
|
||||
align: 'center' as const,
|
||||
field: 'histogram.points',
|
||||
|
@ -181,15 +190,32 @@ export const MonitorListComponent: React.FC<Props> = ({
|
|||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorList.monitoringStatusTitle"
|
||||
defaultMessage="Monitor status"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorList.monitoringStatusTitle"
|
||||
defaultMessage="Monitor status"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<Link to={CERTIFICATES_ROUTE} data-test-subj="uptimeCertificatesLink">
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.monitorList.viewCertificateTitle"
|
||||
defaultMessage="View certificates status"
|
||||
/>
|
||||
</Link>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiBasicTable
|
||||
aria-label={labels.getDescriptionLabel(items.length)}
|
||||
error={error?.message}
|
||||
|
|
|
@ -7,7 +7,7 @@ import React from 'react';
|
|||
import { EuiText, EuiSpacer } from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MonitorPageLink } from '../monitor_page_link';
|
||||
import { MonitorPageLink } from '../../../common/monitor_page_link';
|
||||
import { useGetUrlParams } from '../../../../hooks';
|
||||
import { stringifyUrlParams } from '../../../../lib/helper/stringify_url_params';
|
||||
import { MonitorError } from '../../../../../common/runtime_types';
|
||||
|
|
|
@ -21,6 +21,10 @@ export const HISTORY_COLUMN_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const TLS_COLUMN_LABEL = i18n.translate('xpack.uptime.monitorList.tlsColumnLabel', {
|
||||
defaultMessage: 'TLS Certificate',
|
||||
});
|
||||
|
||||
export const getExpandDrawerLabel = (id: string) => {
|
||||
return i18n.translate('xpack.uptime.monitorList.expandDrawerButton.ariaLabel', {
|
||||
defaultMessage: 'Expand row for monitor with ID {id}',
|
||||
|
|
|
@ -8,3 +8,4 @@ export * from './use_monitor';
|
|||
export * from './use_url_params';
|
||||
export * from './use_telemetry';
|
||||
export * from './update_kuery_string';
|
||||
export * from './use_cert_status';
|
||||
|
|
42
x-pack/plugins/uptime/public/hooks/use_cert_status.ts
Normal file
42
x-pack/plugins/uptime/public/hooks/use_cert_status.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { useSelector } from 'react-redux';
|
||||
import { selectDynamicSettings } from '../state/selectors';
|
||||
import { CERT_STATUS } from '../../common/constants';
|
||||
|
||||
export const useCertStatus = (expiryDate?: string, issueDate?: string) => {
|
||||
const dss = useSelector(selectDynamicSettings);
|
||||
|
||||
const expiryThreshold = dss.settings?.certThresholds?.expiration;
|
||||
|
||||
const ageThreshold = dss.settings?.certThresholds?.age;
|
||||
|
||||
const certValidityDate = new Date(expiryDate ?? '');
|
||||
|
||||
const isValidDate = !isNaN(certValidityDate.valueOf());
|
||||
|
||||
if (!isValidDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isExpiringSoon = moment(certValidityDate).diff(moment(), 'days') < expiryThreshold!;
|
||||
|
||||
const isTooOld = moment().diff(moment(issueDate), 'days') > ageThreshold!;
|
||||
|
||||
const isExpired = moment(certValidityDate) < moment();
|
||||
|
||||
if (isExpired) {
|
||||
return CERT_STATUS.EXPIRED;
|
||||
}
|
||||
|
||||
return isExpiringSoon
|
||||
? CERT_STATUS.EXPIRING_SOON
|
||||
: isTooOld
|
||||
? CERT_STATUS.TOO_OLD
|
||||
: CERT_STATUS.OK;
|
||||
};
|
|
@ -13,6 +13,7 @@ export enum UptimePage {
|
|||
Overview = 'Overview',
|
||||
Monitor = 'Monitor',
|
||||
Settings = 'Settings',
|
||||
Certificates = 'Certificates',
|
||||
NotFound = '__not-found__',
|
||||
}
|
||||
|
||||
|
|
56
x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/certificates.test.tsx.snap
generated
Normal file
56
x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/certificates.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,56 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CertificatesPage shallow renders expected elements for valid props 1`] = `
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"history": Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"canGo": [Function],
|
||||
"createHref": [Function],
|
||||
"entries": Array [
|
||||
Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"index": 0,
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
},
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"key": "TestKeyForTesting",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"match": Object {
|
||||
"isExact": true,
|
||||
"params": Object {},
|
||||
"path": "/",
|
||||
"url": "/",
|
||||
},
|
||||
"staticContext": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CertificatesPage />
|
||||
</ContextProvider>
|
||||
`;
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { shallowWithRouter } from '../../lib';
|
||||
import { CertificatesPage } from '../certificates';
|
||||
|
||||
describe('CertificatesPage', () => {
|
||||
it('shallow renders expected elements for valid props', () => {
|
||||
expect(shallowWithRouter(<CertificatesPage />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
136
x-pack/plugins/uptime/public/pages/certificates.tsx
Normal file
136
x-pack/plugins/uptime/public/pages/certificates.tsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 { Link } from 'react-router-dom';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
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 { 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';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const LOCAL_STORAGE_KEY = 'xpack.uptime.certList.pageSize';
|
||||
const getPageSizeValue = () => {
|
||||
const value = parseInt(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '', 10);
|
||||
if (isNaN(value)) {
|
||||
return DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const CertificatesPage: React.FC = () => {
|
||||
useUptimeTelemetry(UptimePage.Certificates);
|
||||
|
||||
useTrackPageview({ app: 'uptime', path: 'certificates' });
|
||||
useTrackPageview({ app: 'uptime', path: 'certificates', delay: 15000 });
|
||||
|
||||
useBreadcrumbs([{ text: 'Certificates' }]);
|
||||
|
||||
const [page, setPage] = useState({ index: 0, size: getPageSizeValue() });
|
||||
const [sort, setSort] = useState<CertSort>({
|
||||
field: 'not_after',
|
||||
direction: 'asc',
|
||||
});
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { lastRefresh, refreshApp } = useContext(UptimeRefreshContext);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getDynamicSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getCertificatesAction.get({
|
||||
search,
|
||||
...page,
|
||||
sortBy: sort.field,
|
||||
direction: sort.direction,
|
||||
})
|
||||
);
|
||||
}, [dispatch, page, search, sort.direction, sort.field, lastRefresh]);
|
||||
|
||||
const certificates = useSelector(certificatesSelector);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false} style={{ marginRight: 'auto', alignSelf: 'center' }}>
|
||||
<Link to={OVERVIEW_ROUTE} data-test-subj="uptimeCertificatesToOverviewLink">
|
||||
<EuiButtonEmpty size="s" color="primary" iconType="arrowLeft">
|
||||
{labels.RETURN_TO_OVERVIEW}
|
||||
</EuiButtonEmpty>
|
||||
</Link>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
|
||||
<Link to={SETTINGS_ROUTE} data-test-subj="uptimeCertificatesToOverviewLink">
|
||||
<EuiButtonEmpty size="s" color="primary" iconType="gear">
|
||||
{labels.SETTINGS_ON_CERT}
|
||||
</EuiButtonEmpty>
|
||||
</Link>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="refresh"
|
||||
onClick={() => {
|
||||
refreshApp();
|
||||
}}
|
||||
data-test-subj="superDatePickerApplyTimeButton"
|
||||
>
|
||||
{labels.REFRESH_CERT}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel>
|
||||
<PageHeader
|
||||
headingText={
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.certificates.heading"
|
||||
defaultMessage="TLS Certificates ({total})"
|
||||
values={{
|
||||
total: <span data-test-subj="uptimeCertTotal">{certificates?.total ?? 0}</span>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
datePicker={false}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<CertificateSearch setSearch={setSearch} />
|
||||
<EuiSpacer size="m" />
|
||||
<CertificateList
|
||||
page={page}
|
||||
onChange={(pageVal, sortVal) => {
|
||||
setPage(pageVal);
|
||||
setSort(sortVal);
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, pageVal.size.toString());
|
||||
}}
|
||||
sort={sort}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { monitorStatusSelector } from '../state/selectors';
|
||||
import { PageHeader } from './page_header';
|
||||
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
|
||||
|
@ -14,8 +14,15 @@ import { useTrackPageview } from '../../../observability/public';
|
|||
import { useMonitorId, useUptimeTelemetry, UptimePage } from '../hooks';
|
||||
import { MonitorCharts } from '../components/monitor';
|
||||
import { MonitorStatusDetails, PingList } from '../components/monitor';
|
||||
import { getDynamicSettings } from '../state/actions/dynamic_settings';
|
||||
|
||||
export const MonitorPage: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getDynamicSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
const monitorId = useMonitorId();
|
||||
|
||||
const selectedMonitor = useSelector(monitorStatusSelector);
|
||||
|
|
|
@ -13,7 +13,7 @@ import { SETTINGS_ROUTE } from '../../common/constants';
|
|||
import { ToggleAlertFlyoutButton } from '../components/overview/alerts/alerts_containers';
|
||||
|
||||
interface PageHeaderProps {
|
||||
headingText: string;
|
||||
headingText: string | JSX.Element;
|
||||
extraLinks?: boolean;
|
||||
datePicker?: boolean;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,21 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SETTINGS_ON_CERT = i18n.translate('xpack.uptime.certificates.settingsLinkLabel', {
|
||||
defaultMessage: 'Settings',
|
||||
});
|
||||
|
||||
export const RETURN_TO_OVERVIEW = i18n.translate(
|
||||
'xpack.uptime.certificates.returnToOverviewLinkLabel',
|
||||
{
|
||||
defaultMessage: 'Return to overview',
|
||||
}
|
||||
);
|
||||
|
||||
export const REFRESH_CERT = i18n.translate('xpack.uptime.certificates.refresh', {
|
||||
defaultMessage: 'Refresh',
|
||||
});
|
||||
|
||||
export const settings = {
|
||||
breadcrumbText: i18n.translate('xpack.uptime.settingsBreadcrumbText', {
|
||||
defaultMessage: 'Settings',
|
||||
|
|
|
@ -8,8 +8,14 @@ import React, { FC } from 'react';
|
|||
import { Route, Switch } from 'react-router-dom';
|
||||
import { DataPublicPluginSetup } from '../../../../src/plugins/data/public';
|
||||
import { OverviewPage } from './components/overview/overview_container';
|
||||
import { MONITOR_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../common/constants';
|
||||
import {
|
||||
CERTIFICATES_ROUTE,
|
||||
MONITOR_ROUTE,
|
||||
OVERVIEW_ROUTE,
|
||||
SETTINGS_ROUTE,
|
||||
} from '../common/constants';
|
||||
import { MonitorPage, NotFoundPage, SettingsPage } from './pages';
|
||||
import { CertificatesPage } from './pages/certificates';
|
||||
|
||||
interface RouterProps {
|
||||
autocomplete: DataPublicPluginSetup['autocomplete'];
|
||||
|
@ -27,6 +33,11 @@ export const PageRouter: FC<RouterProps> = ({ autocomplete }) => (
|
|||
<SettingsPage />
|
||||
</div>
|
||||
</Route>
|
||||
<Route path={CERTIFICATES_ROUTE}>
|
||||
<div data-test-subj="uptimeCertificatesPage">
|
||||
<CertificatesPage />
|
||||
</div>
|
||||
</Route>
|
||||
<Route path={OVERVIEW_ROUTE}>
|
||||
<div data-test-subj="uptimeOverviewPage">
|
||||
<OverviewPage autocomplete={autocomplete} />
|
||||
|
|
13
x-pack/plugins/uptime/public/state/api/certificates.ts
Normal file
13
x-pack/plugins/uptime/public/state/api/certificates.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { API_URLS } from '../../../common/constants';
|
||||
import { apiService } from './utils';
|
||||
import { CertResultType, GetCertsParams } from '../../../common/runtime_types';
|
||||
|
||||
export const fetchCertificates = async (params: GetCertsParams) => {
|
||||
return await apiService.get(API_URLS.CERTS, params, CertResultType);
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { handleActions } from 'redux-actions';
|
||||
import { takeLatest } from 'redux-saga/effects';
|
||||
import { createAsyncAction } from '../actions/utils';
|
||||
import { getAsyncInitialState, handleAsyncAction } from '../reducers/utils';
|
||||
import { CertResult, GetCertsParams } from '../../../common/runtime_types';
|
||||
import { AppState } from '../index';
|
||||
import { AsyncInitialState } from '../reducers/types';
|
||||
import { fetchEffectFactory } from '../effects/fetch_effect';
|
||||
import { fetchCertificates } from '../api/certificates';
|
||||
|
||||
export const getCertificatesAction = createAsyncAction<GetCertsParams, CertResult>(
|
||||
'GET_CERTIFICATES'
|
||||
);
|
||||
|
||||
interface CertificatesState {
|
||||
certs: AsyncInitialState<CertResult>;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
certs: getAsyncInitialState(),
|
||||
};
|
||||
|
||||
export const certificatesReducer = handleActions<CertificatesState>(
|
||||
{
|
||||
...handleAsyncAction<CertificatesState>('certs', getCertificatesAction),
|
||||
},
|
||||
initialState
|
||||
);
|
||||
|
||||
export function* fetchCertificatesEffect() {
|
||||
yield takeLatest(
|
||||
getCertificatesAction.get,
|
||||
fetchEffectFactory(fetchCertificates, getCertificatesAction.success, getCertificatesAction.fail)
|
||||
);
|
||||
}
|
||||
|
||||
export const certificatesSelector = ({ certificates }: AppState) => certificates.certs.data;
|
|
@ -16,6 +16,7 @@ import { fetchPingsEffect, fetchPingHistogramEffect } from './ping';
|
|||
import { fetchMonitorDurationEffect } from './monitor_duration';
|
||||
import { fetchMLJobEffect } from './ml_anomaly';
|
||||
import { fetchIndexStatusEffect } from './index_status';
|
||||
import { fetchCertificatesEffect } from '../certificates/certificates';
|
||||
|
||||
export function* rootEffect() {
|
||||
yield fork(fetchMonitorDetailsEffect);
|
||||
|
@ -31,4 +32,5 @@ export function* rootEffect() {
|
|||
yield fork(fetchMLJobEffect);
|
||||
yield fork(fetchMonitorDurationEffect);
|
||||
yield fork(fetchIndexStatusEffect);
|
||||
yield fork(fetchCertificatesEffect);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import { pingListReducer } from './ping_list';
|
|||
import { monitorDurationReducer } from './monitor_duration';
|
||||
import { indexStatusReducer } from './index_status';
|
||||
import { mlJobsReducer } from './ml_anomaly';
|
||||
import { certificatesReducer } from '../certificates/certificates';
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
monitor: monitorReducer,
|
||||
|
@ -33,4 +34,5 @@ export const rootReducer = combineReducers({
|
|||
ml: mlJobsReducer,
|
||||
monitorDuration: monitorDurationReducer,
|
||||
indexStatus: indexStatusReducer,
|
||||
certificates: certificatesReducer,
|
||||
});
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
getMLCapabilitiesAction,
|
||||
} from '../actions';
|
||||
import { getAsyncInitialState, handleAsyncAction } from './utils';
|
||||
import { IHttpFetchError } from '../../../../../../target/types/core/public/http';
|
||||
import { AsyncInitialState } from './types';
|
||||
import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities';
|
||||
import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types';
|
||||
|
@ -37,15 +36,13 @@ const initialState: MLJobState = {
|
|||
mlCapabilities: getAsyncInitialState(),
|
||||
};
|
||||
|
||||
type Payload = IHttpFetchError;
|
||||
|
||||
export const mlJobsReducer = handleActions<MLJobState>(
|
||||
{
|
||||
...handleAsyncAction<MLJobState, Payload>('mlJob', getExistingMLJobAction),
|
||||
...handleAsyncAction<MLJobState, Payload>('mlCapabilities', getMLCapabilitiesAction),
|
||||
...handleAsyncAction<MLJobState, Payload>('createJob', createMLJobAction),
|
||||
...handleAsyncAction<MLJobState, Payload>('deleteJob', deleteMLJobAction),
|
||||
...handleAsyncAction<MLJobState, Payload>('anomalies', getAnomalyRecordsAction),
|
||||
...handleAsyncAction<MLJobState>('mlJob', getExistingMLJobAction),
|
||||
...handleAsyncAction<MLJobState>('mlCapabilities', getMLCapabilitiesAction),
|
||||
...handleAsyncAction<MLJobState>('createJob', createMLJobAction),
|
||||
...handleAsyncAction<MLJobState>('deleteJob', deleteMLJobAction),
|
||||
...handleAsyncAction<MLJobState>('anomalies', getAnomalyRecordsAction),
|
||||
...{
|
||||
[String(resetMLState)]: state => ({
|
||||
...state,
|
||||
|
|
|
@ -9,9 +9,9 @@ import { getMonitorList, getMonitorListSuccess, getMonitorListFailure } from '..
|
|||
import { MonitorSummaryResult } from '../../../common/runtime_types';
|
||||
|
||||
export interface MonitorList {
|
||||
list: MonitorSummaryResult;
|
||||
error?: Error;
|
||||
loading: boolean;
|
||||
list: MonitorSummaryResult;
|
||||
}
|
||||
|
||||
export const initialState: MonitorList = {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { Action } from 'redux-actions';
|
||||
import { AsyncAction } from '../actions/types';
|
||||
|
||||
export function handleAsyncAction<ReducerState, Payload>(
|
||||
export function handleAsyncAction<ReducerState>(
|
||||
storeKey: string,
|
||||
asyncAction: AsyncAction<any, any>
|
||||
) {
|
||||
|
@ -24,7 +24,7 @@ export function handleAsyncAction<ReducerState, Payload>(
|
|||
...state,
|
||||
[storeKey]: {
|
||||
...(state as any)[storeKey],
|
||||
data: action.payload === null ? action.payload : { ...action.payload },
|
||||
data: action.payload,
|
||||
loading: false,
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -101,6 +101,12 @@ describe('state selectors', () => {
|
|||
loading: false,
|
||||
},
|
||||
},
|
||||
certificates: {
|
||||
certs: {
|
||||
data: null,
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('selects base path from state', () => {
|
||||
|
|
|
@ -19,9 +19,10 @@ describe('getCerts', () => {
|
|||
_score: 0,
|
||||
_source: {
|
||||
tls: {
|
||||
certificate_not_valid_before: '2019-08-16T01:40:25.000Z',
|
||||
server: {
|
||||
x509: {
|
||||
not_before: '2019-08-16T01:40:25.000Z',
|
||||
not_after: '2020-07-16T03:15:39.000Z',
|
||||
subject: {
|
||||
common_name: 'r2.shared.global.fastly.net',
|
||||
},
|
||||
|
@ -34,12 +35,14 @@ describe('getCerts', () => {
|
|||
sha256: '12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d',
|
||||
},
|
||||
},
|
||||
certificate_not_valid_after: '2020-07-16T03:15:39.000Z',
|
||||
},
|
||||
monitor: {
|
||||
name: 'Real World Test',
|
||||
id: 'real-world-test',
|
||||
},
|
||||
url: {
|
||||
full: 'https://fullurl.com',
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
'tls.server.hash.sha256': [
|
||||
|
@ -96,24 +99,30 @@ describe('getCerts', () => {
|
|||
to: 'now+1h',
|
||||
search: 'my_common_name',
|
||||
size: 30,
|
||||
sortBy: 'not_after',
|
||||
direction: 'desc',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"certificate_not_valid_after": "2020-07-16T03:15:39.000Z",
|
||||
"certificate_not_valid_before": "2019-08-16T01:40:25.000Z",
|
||||
"common_name": "r2.shared.global.fastly.net",
|
||||
"issuer": "GlobalSign CloudSSL CA - SHA256 - G3",
|
||||
"monitors": Array [
|
||||
Object {
|
||||
"id": "real-world-test",
|
||||
"name": "Real World Test",
|
||||
},
|
||||
],
|
||||
"sha1": "b7b4b89ef0d0caf39d223736f0fdbb03c7b426f1",
|
||||
"sha256": "12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d",
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"certs": Array [
|
||||
Object {
|
||||
"common_name": "r2.shared.global.fastly.net",
|
||||
"issuer": "GlobalSign CloudSSL CA - SHA256 - G3",
|
||||
"monitors": Array [
|
||||
Object {
|
||||
"id": "real-world-test",
|
||||
"name": "Real World Test",
|
||||
"url": undefined,
|
||||
},
|
||||
],
|
||||
"not_after": "2020-07-16T03:15:39.000Z",
|
||||
"not_before": "2019-08-16T01:40:25.000Z",
|
||||
"sha1": "b7b4b89ef0d0caf39d223736f0fdbb03c7b426f1",
|
||||
"sha256": "12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d",
|
||||
},
|
||||
],
|
||||
"total": 0,
|
||||
}
|
||||
`);
|
||||
expect(mockCallES.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -128,9 +137,16 @@ describe('getCerts', () => {
|
|||
"tls.server.x509.subject.common_name",
|
||||
"tls.server.hash.sha1",
|
||||
"tls.server.hash.sha256",
|
||||
"tls.certificate_not_valid_before",
|
||||
"tls.certificate_not_valid_after",
|
||||
"tls.server.x509.not_after",
|
||||
"tls.server.x509.not_before",
|
||||
],
|
||||
"aggs": Object {
|
||||
"total": Object {
|
||||
"cardinality": Object {
|
||||
"field": "tls.server.hash.sha256",
|
||||
},
|
||||
},
|
||||
},
|
||||
"collapse": Object {
|
||||
"field": "tls.server.hash.sha256",
|
||||
"inner_hits": Object {
|
||||
|
@ -138,6 +154,7 @@ describe('getCerts', () => {
|
|||
"includes": Array [
|
||||
"monitor.id",
|
||||
"monitor.name",
|
||||
"url.full",
|
||||
],
|
||||
},
|
||||
"collapse": Object {
|
||||
|
@ -151,13 +168,13 @@ describe('getCerts', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
"from": 1,
|
||||
"from": 30,
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "tls",
|
||||
"field": "tls.server",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
|
@ -169,39 +186,32 @@ describe('getCerts', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"wildcard": Object {
|
||||
"tls.server.issuer": Object {
|
||||
"value": "*my_common_name*",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"wildcard": Object {
|
||||
"tls.common_name": Object {
|
||||
"value": "*my_common_name*",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"wildcard": Object {
|
||||
"monitor.id": Object {
|
||||
"value": "*my_common_name*",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"wildcard": Object {
|
||||
"monitor.name": Object {
|
||||
"value": "*my_common_name*",
|
||||
},
|
||||
"multi_match": Object {
|
||||
"fields": Array [
|
||||
"monitor.id.text",
|
||||
"monitor.name.text",
|
||||
"url.full.text",
|
||||
"tls.server.x509.subject.common_name.text",
|
||||
"tls.server.x509.issuer.common_name.text",
|
||||
],
|
||||
"query": "my_common_name",
|
||||
"type": "phrase_prefix",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 30,
|
||||
"sort": Array [
|
||||
Object {
|
||||
"tls.server.x509.not_after": Object {
|
||||
"order": "desc",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"index": "heartbeat*",
|
||||
},
|
||||
|
|
|
@ -32,7 +32,14 @@ describe('getLatestMonitor', () => {
|
|||
},
|
||||
},
|
||||
size: 1,
|
||||
_source: ['url', 'monitor', 'observer', 'tls', '@timestamp'],
|
||||
_source: [
|
||||
'url',
|
||||
'monitor',
|
||||
'observer',
|
||||
'@timestamp',
|
||||
'tls.server.x509.not_after',
|
||||
'tls.server.x509.not_before',
|
||||
],
|
||||
sort: {
|
||||
'@timestamp': { order: 'desc' },
|
||||
},
|
||||
|
@ -83,6 +90,10 @@ describe('getLatestMonitor', () => {
|
|||
"type": "http",
|
||||
},
|
||||
"timestamp": "123456",
|
||||
"tls": Object {
|
||||
"not_after": undefined,
|
||||
"not_before": undefined,
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(result.timestamp).toBe('123456');
|
||||
|
|
|
@ -5,9 +5,16 @@
|
|||
*/
|
||||
|
||||
import { UMElasticsearchQueryFn } from '../adapters';
|
||||
import { Cert, GetCertsParams } from '../../../common/runtime_types';
|
||||
import { CertResult, GetCertsParams } from '../../../common/runtime_types';
|
||||
|
||||
export const getCerts: UMElasticsearchQueryFn<GetCertsParams, Cert[]> = async ({
|
||||
enum SortFields {
|
||||
'issuer' = 'tls.server.x509.issuer.common_name',
|
||||
'not_after' = 'tls.server.x509.not_after',
|
||||
'not_before' = 'tls.server.x509.not_before',
|
||||
'common_name' = 'tls.server.x509.subject.common_name',
|
||||
}
|
||||
|
||||
export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = async ({
|
||||
callES,
|
||||
dynamicSettings,
|
||||
index,
|
||||
|
@ -15,19 +22,29 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, Cert[]> = async ({
|
|||
to,
|
||||
search,
|
||||
size,
|
||||
sortBy,
|
||||
direction,
|
||||
}) => {
|
||||
const searchWrapper = `*${search}*`;
|
||||
const sort = SortFields[sortBy as keyof typeof SortFields];
|
||||
|
||||
const params: any = {
|
||||
index: dynamicSettings.heartbeatIndices,
|
||||
body: {
|
||||
from: index,
|
||||
from: index * size,
|
||||
size,
|
||||
sort: [
|
||||
{
|
||||
[sort]: {
|
||||
order: direction,
|
||||
},
|
||||
},
|
||||
],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
exists: {
|
||||
field: 'tls',
|
||||
field: 'tls.server',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -48,14 +65,14 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, Cert[]> = async ({
|
|||
'tls.server.x509.subject.common_name',
|
||||
'tls.server.hash.sha1',
|
||||
'tls.server.hash.sha256',
|
||||
'tls.certificate_not_valid_before',
|
||||
'tls.certificate_not_valid_after',
|
||||
'tls.server.x509.not_after',
|
||||
'tls.server.x509.not_before',
|
||||
],
|
||||
collapse: {
|
||||
field: 'tls.server.hash.sha256',
|
||||
inner_hits: {
|
||||
_source: {
|
||||
includes: ['monitor.id', 'monitor.name'],
|
||||
includes: ['monitor.id', 'monitor.name', 'url.full'],
|
||||
},
|
||||
collapse: {
|
||||
field: 'monitor.id',
|
||||
|
@ -64,72 +81,67 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, Cert[]> = async ({
|
|||
sort: [{ 'monitor.id': 'asc' }],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
total: {
|
||||
cardinality: {
|
||||
field: 'tls.server.hash.sha256',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (search) {
|
||||
params.body.query.bool.minimum_should_match = 1;
|
||||
params.body.query.bool.should = [
|
||||
{
|
||||
wildcard: {
|
||||
'tls.server.issuer': {
|
||||
value: searchWrapper,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
wildcard: {
|
||||
'tls.common_name': {
|
||||
value: searchWrapper,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
wildcard: {
|
||||
'monitor.id': {
|
||||
value: searchWrapper,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
wildcard: {
|
||||
'monitor.name': {
|
||||
value: searchWrapper,
|
||||
},
|
||||
multi_match: {
|
||||
query: escape(search),
|
||||
type: 'phrase_prefix',
|
||||
fields: [
|
||||
'monitor.id.text',
|
||||
'monitor.name.text',
|
||||
'url.full.text',
|
||||
'tls.server.x509.subject.common_name.text',
|
||||
'tls.server.x509.issuer.common_name.text',
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const result = await callES('search', params);
|
||||
const formatted = (result?.hits?.hits ?? []).map((hit: any) => {
|
||||
|
||||
const certs = (result?.hits?.hits ?? []).map((hit: any) => {
|
||||
const {
|
||||
_source: {
|
||||
tls: {
|
||||
server: {
|
||||
x509: {
|
||||
issuer: { common_name: issuer },
|
||||
subject: { common_name },
|
||||
},
|
||||
hash: { sha1, sha256 },
|
||||
},
|
||||
certificate_not_valid_after,
|
||||
certificate_not_valid_before,
|
||||
},
|
||||
tls: { server },
|
||||
},
|
||||
} = hit;
|
||||
|
||||
const notAfter = server?.x509?.not_after;
|
||||
const notBefore = server?.x509?.not_before;
|
||||
const issuer = server?.x509?.issuer?.common_name;
|
||||
const commonName = server?.x509?.subject?.common_name;
|
||||
const sha1 = server?.hash?.sha1;
|
||||
const sha256 = server?.hash?.sha256;
|
||||
|
||||
const monitors = hit.inner_hits.monitors.hits.hits.map((monitor: any) => ({
|
||||
name: monitor._source?.monitor.name,
|
||||
id: monitor._source?.monitor.id,
|
||||
url: monitor._source?.url?.full,
|
||||
}));
|
||||
|
||||
return {
|
||||
monitors,
|
||||
certificate_not_valid_after,
|
||||
certificate_not_valid_before,
|
||||
issuer,
|
||||
sha1,
|
||||
sha256,
|
||||
common_name,
|
||||
not_after: notAfter,
|
||||
not_before: notBefore,
|
||||
common_name: commonName,
|
||||
};
|
||||
});
|
||||
return formatted;
|
||||
const total = result?.aggregations?.total?.value ?? 0;
|
||||
return { certs, total };
|
||||
};
|
||||
|
|
|
@ -45,7 +45,14 @@ export const getLatestMonitor: UMElasticsearchQueryFn<GetLatestMonitorParams, Pi
|
|||
},
|
||||
},
|
||||
size: 1,
|
||||
_source: ['url', 'monitor', 'observer', 'tls', '@timestamp'],
|
||||
_source: [
|
||||
'url',
|
||||
'monitor',
|
||||
'observer',
|
||||
'@timestamp',
|
||||
'tls.server.x509.not_after',
|
||||
'tls.server.x509.not_before',
|
||||
],
|
||||
sort: {
|
||||
'@timestamp': { order: 'desc' },
|
||||
},
|
||||
|
@ -54,8 +61,13 @@ export const getLatestMonitor: UMElasticsearchQueryFn<GetLatestMonitorParams, Pi
|
|||
|
||||
const result = await callES('search', params);
|
||||
const doc = result.hits?.hits?.[0];
|
||||
const source = doc?._source ?? {};
|
||||
const docId = doc?._id ?? '';
|
||||
const { tls, ...ping } = doc?._source ?? {};
|
||||
|
||||
return { ...source, docId, timestamp: source['@timestamp'] };
|
||||
return {
|
||||
...ping,
|
||||
docId,
|
||||
timestamp: ping['@timestamp'],
|
||||
tls: { not_after: tls?.server?.x509?.not_after, not_before: tls?.server?.x509?.not_before },
|
||||
};
|
||||
};
|
||||
|
|
|
@ -137,11 +137,11 @@ export const enrichMonitorGroups: MonitorEnricher = async (
|
|||
if (curCheck.tls == null) {
|
||||
curCheck.tls = new HashMap();
|
||||
}
|
||||
if (!doc["tls.certificate_not_valid_after"].isEmpty()) {
|
||||
curCheck.tls.certificate_not_valid_after = doc["tls.certificate_not_valid_after"][0];
|
||||
if (!doc["tls.server.x509.not_after"].isEmpty()) {
|
||||
curCheck.tls.not_after = doc["tls.server.x509.not_after"][0];
|
||||
}
|
||||
if (!doc["tls.certificate_not_valid_before"].isEmpty()) {
|
||||
curCheck.tls.certificate_not_valid_before = doc["tls.certificate_not_valid_before"][0];
|
||||
if (!doc["tls.server.x509.not_before"].isEmpty()) {
|
||||
curCheck.tls.not_before = doc["tls.server.x509.not_before"][0];
|
||||
}
|
||||
|
||||
state.checksByAgentIdIP[agentIdIP] = curCheck;
|
||||
|
|
|
@ -6,19 +6,20 @@
|
|||
|
||||
import { UMElasticsearchQueryFn } from '../adapters';
|
||||
import {
|
||||
HistogramResult,
|
||||
Ping,
|
||||
PingsResponse,
|
||||
GetCertsParams,
|
||||
GetPingsParams,
|
||||
Cert,
|
||||
OverviewFilters,
|
||||
MonitorDetails,
|
||||
MonitorLocations,
|
||||
Snapshot,
|
||||
StatesIndexStatus,
|
||||
HistogramResult,
|
||||
Ping,
|
||||
PingsResponse,
|
||||
GetCertsParams,
|
||||
GetPingsParams,
|
||||
CertResult,
|
||||
} from '../../../common/runtime_types';
|
||||
import { MonitorDurationResult } from '../../../common/types';
|
||||
|
||||
import {
|
||||
GetFilterBarParams,
|
||||
GetLatestMonitorParams,
|
||||
|
@ -36,7 +37,7 @@ import { GetSnapshotCountParams } from './get_snapshot_counts';
|
|||
type ESQ<P, R> = UMElasticsearchQueryFn<P, R>;
|
||||
|
||||
export interface UptimeRequests {
|
||||
getCerts: ESQ<GetCertsParams, Cert[]>;
|
||||
getCerts: ESQ<GetCertsParams, CertResult>;
|
||||
getFilterBar: ESQ<GetFilterBarParams, OverviewFilters>;
|
||||
getIndexPattern: ESQ<{}, {}>;
|
||||
getLatestMonitor: ESQ<GetLatestMonitorParams, Ping>;
|
||||
|
|
|
@ -5,14 +5,16 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { UMServerLibs } from '../lib/lib';
|
||||
import { UMRestApiRouteFactory } from '.';
|
||||
import { API_URLS } from '../../common/constants';
|
||||
import { API_URLS } from '../../../common/constants';
|
||||
import { UMServerLibs } from '../../lib/lib';
|
||||
import { UMRestApiRouteFactory } from '../types';
|
||||
|
||||
const DEFAULT_INDEX = 0;
|
||||
const DEFAULT_SIZE = 25;
|
||||
const DEFAULT_FROM = 'now-1d';
|
||||
const DEFAULT_TO = 'now';
|
||||
const DEFAULT_SORT = 'not_after';
|
||||
const DEFAULT_DIRECTION = 'asc';
|
||||
|
||||
export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
|
||||
method: 'GET',
|
||||
|
@ -24,30 +26,33 @@ export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) =
|
|||
search: schema.maybe(schema.string()),
|
||||
index: schema.maybe(schema.number()),
|
||||
size: schema.maybe(schema.number()),
|
||||
sortBy: schema.maybe(schema.string()),
|
||||
direction: schema.maybe(schema.string()),
|
||||
}),
|
||||
},
|
||||
writeAccess: false,
|
||||
options: {
|
||||
tags: ['access:uptime-read'],
|
||||
},
|
||||
handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => {
|
||||
const index = request.query?.index ?? DEFAULT_INDEX;
|
||||
const size = request.query?.size ?? DEFAULT_SIZE;
|
||||
const from = request.query?.from ?? DEFAULT_FROM;
|
||||
const to = request.query?.to ?? DEFAULT_TO;
|
||||
const sortBy = request.query?.sortBy ?? DEFAULT_SORT;
|
||||
const direction = request.query?.direction ?? DEFAULT_DIRECTION;
|
||||
const { search } = request.query;
|
||||
|
||||
const result = await libs.requests.getCerts({
|
||||
callES,
|
||||
dynamicSettings,
|
||||
index,
|
||||
search,
|
||||
size,
|
||||
from,
|
||||
to,
|
||||
sortBy,
|
||||
direction,
|
||||
});
|
||||
return response.ok({
|
||||
body: {
|
||||
certs: await libs.requests.getCerts({
|
||||
callES,
|
||||
dynamicSettings,
|
||||
index,
|
||||
search,
|
||||
size,
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
certs: result.certs,
|
||||
total: result.total,
|
||||
},
|
||||
});
|
||||
},
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createGetCertsRoute } from './certs';
|
||||
import { createGetCertsRoute } from './certs/certs';
|
||||
import { createGetOverviewFilters } from './overview_filters';
|
||||
import { createGetPingHistogramRoute, createGetPingsRoute } from './pings';
|
||||
import { createGetDynamicSettingsRoute, createPostDynamicSettingsRoute } from './dynamic_settings';
|
||||
|
@ -19,6 +19,7 @@ import {
|
|||
} from './monitors';
|
||||
import { createGetMonitorDurationRoute } from './monitors/monitors_durations';
|
||||
import { createGetIndexPatternRoute, createGetIndexStatusRoute } from './index_state';
|
||||
|
||||
export * from './types';
|
||||
export { createRouteWithAuth } from './create_route_with_auth';
|
||||
export { uptimeRouteWrapper } from './uptime_route_wrapper';
|
||||
|
|
|
@ -21,7 +21,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
describe('empty index', async () => {
|
||||
it('returns empty array for no data', async () => {
|
||||
const apiResponse = await supertest.get(API_URLS.CERTS);
|
||||
expect(JSON.stringify(apiResponse.body)).to.eql('{"certs":[]}');
|
||||
expect(JSON.stringify(apiResponse.body)).to.eql('{"certs":[],"total":0}');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -39,10 +39,10 @@ export default function({ getService }: FtrProviderContext) {
|
|||
10000,
|
||||
{
|
||||
tls: {
|
||||
certificate_not_valid_after: cnva,
|
||||
certificate_not_valid_before: cnvb,
|
||||
server: {
|
||||
x509: {
|
||||
not_after: cnva,
|
||||
not_before: cnvb,
|
||||
issuer: {
|
||||
common_name: 'issuer-common-name',
|
||||
},
|
||||
|
@ -78,9 +78,12 @@ export default function({ getService }: FtrProviderContext) {
|
|||
|
||||
const cert = body.certs[0];
|
||||
expect(Array.isArray(cert.monitors)).to.be(true);
|
||||
expect(cert.monitors[0]).to.eql({ id: monitorId });
|
||||
expect(cert.certificate_not_valid_after).to.eql(cnva);
|
||||
expect(cert.certificate_not_valid_before).to.eql(cnvb);
|
||||
expect(cert.monitors[0]).to.eql({
|
||||
id: monitorId,
|
||||
url: 'http://localhost:5678/pattern?r=200x5,500x1',
|
||||
});
|
||||
expect(cert.not_after).to.eql(cnva);
|
||||
expect(cert.not_before).to.eql(cnvb);
|
||||
expect(cert.common_name).to.eql('subject-common-name');
|
||||
expect(cert.issuer).to.eql('issuer-common-name');
|
||||
});
|
||||
|
|
|
@ -27,5 +27,6 @@
|
|||
"full": "http://localhost:5678/pattern?r=200x1"
|
||||
},
|
||||
"docId": "h5toHm0B0I9WX_CznN_V",
|
||||
"timestamp": "2019-09-11T03:40:34.371Z"
|
||||
}
|
||||
"timestamp": "2019-09-11T03:40:34.371Z",
|
||||
"tls": {}
|
||||
}
|
||||
|
|
|
@ -6,119 +6,36 @@
|
|||
|
||||
import uuid from 'uuid';
|
||||
import { merge, flattenDeep } from 'lodash';
|
||||
import { makePing } from './make_ping';
|
||||
import { TlsProps } from './make_tls';
|
||||
|
||||
const INDEX_NAME = 'heartbeat-8-generated-test';
|
||||
interface CheckProps {
|
||||
es: any;
|
||||
monitorId?: string;
|
||||
numIps?: number;
|
||||
fields?: { [key: string]: any };
|
||||
mogrify?: (doc: any) => any;
|
||||
refresh?: boolean;
|
||||
tls?: boolean | TlsProps;
|
||||
}
|
||||
|
||||
export const makePing = async (
|
||||
es: any,
|
||||
monitorId: string,
|
||||
fields: { [key: string]: any },
|
||||
mogrify: (doc: any) => any,
|
||||
refresh: boolean = true
|
||||
) => {
|
||||
const baseDoc = {
|
||||
tcp: {
|
||||
rtt: {
|
||||
connect: {
|
||||
us: 14687,
|
||||
},
|
||||
},
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'mpls',
|
||||
location: '37.926868, -78.024902',
|
||||
},
|
||||
hostname: 'avc-x1e',
|
||||
},
|
||||
agent: {
|
||||
hostname: 'avc-x1e',
|
||||
id: '10730a1a-4cb7-45ce-8524-80c4820476ab',
|
||||
type: 'heartbeat',
|
||||
ephemeral_id: '0d9a8dc6-f604-49e3-86a0-d8f9d6f2cbad',
|
||||
version: '8.0.0',
|
||||
},
|
||||
'@timestamp': new Date().toISOString(),
|
||||
resolve: {
|
||||
rtt: {
|
||||
us: 350,
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
},
|
||||
ecs: {
|
||||
version: '1.1.0',
|
||||
},
|
||||
host: {
|
||||
name: 'avc-x1e',
|
||||
},
|
||||
http: {
|
||||
rtt: {
|
||||
response_header: {
|
||||
us: 19349,
|
||||
},
|
||||
total: {
|
||||
us: 48954,
|
||||
},
|
||||
write_request: {
|
||||
us: 33,
|
||||
},
|
||||
content: {
|
||||
us: 51,
|
||||
},
|
||||
validate: {
|
||||
us: 19400,
|
||||
},
|
||||
},
|
||||
response: {
|
||||
status_code: 200,
|
||||
body: {
|
||||
bytes: 3,
|
||||
hash: '27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf',
|
||||
},
|
||||
},
|
||||
},
|
||||
monitor: {
|
||||
duration: {
|
||||
us: 49347,
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
id: monitorId,
|
||||
check_group: uuid.v4(),
|
||||
type: 'http',
|
||||
status: 'up',
|
||||
},
|
||||
event: {
|
||||
dataset: 'uptime',
|
||||
},
|
||||
url: {
|
||||
path: '/pattern',
|
||||
scheme: 'http',
|
||||
port: 5678,
|
||||
domain: 'localhost',
|
||||
query: 'r=200x5,500x1',
|
||||
full: 'http://localhost:5678/pattern?r=200x5,500x1',
|
||||
},
|
||||
};
|
||||
|
||||
const doc = mogrify(merge(baseDoc, fields));
|
||||
|
||||
await es.index({
|
||||
index: INDEX_NAME,
|
||||
refresh,
|
||||
body: doc,
|
||||
});
|
||||
|
||||
return doc;
|
||||
const getRandomMonitorId = () => {
|
||||
return (
|
||||
'monitor-' +
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.substring(7)
|
||||
);
|
||||
};
|
||||
|
||||
export const makeCheck = async (
|
||||
es: any,
|
||||
monitorId: string,
|
||||
numIps: number,
|
||||
fields: { [key: string]: any },
|
||||
mogrify: (doc: any) => any,
|
||||
refresh: boolean = true
|
||||
) => {
|
||||
export const makeCheck = async ({
|
||||
es,
|
||||
monitorId = getRandomMonitorId(),
|
||||
numIps = 1,
|
||||
fields = {},
|
||||
mogrify = d => d,
|
||||
refresh = true,
|
||||
tls = false,
|
||||
}: CheckProps): Promise<{ monitorId: string; docs: any }> => {
|
||||
const cgFields = {
|
||||
monitor: {
|
||||
check_group: uuid.v4(),
|
||||
|
@ -139,7 +56,7 @@ export const makeCheck = async (
|
|||
if (i === numIps - 1) {
|
||||
pingFields.summary = summary;
|
||||
}
|
||||
const doc = await makePing(es, monitorId, pingFields, mogrify, false);
|
||||
const doc = await makePing(es, monitorId, pingFields, mogrify, false, tls as any);
|
||||
docs.push(doc);
|
||||
// @ts-ignore
|
||||
summary[doc.monitor.status]++;
|
||||
|
@ -149,15 +66,15 @@ export const makeCheck = async (
|
|||
await es.indices.refresh();
|
||||
}
|
||||
|
||||
return docs;
|
||||
return { monitorId, docs };
|
||||
};
|
||||
|
||||
export const makeChecks = async (
|
||||
es: any,
|
||||
monitorId: string,
|
||||
numChecks: number,
|
||||
numIps: number,
|
||||
every: number, // number of millis between checks
|
||||
numChecks: number = 1,
|
||||
numIps: number = 1,
|
||||
every: number = 10000, // number of millis between checks
|
||||
fields: { [key: string]: any } = {},
|
||||
mogrify: (doc: any) => any = d => d,
|
||||
refresh: boolean = true
|
||||
|
@ -177,7 +94,8 @@ export const makeChecks = async (
|
|||
},
|
||||
},
|
||||
});
|
||||
checks.push(await makeCheck(es, monitorId, numIps, fields, mogrify, false));
|
||||
const { docs } = await makeCheck({ es, monitorId, numIps, fields, mogrify, refresh: false });
|
||||
checks.push(docs);
|
||||
}
|
||||
|
||||
if (refresh) {
|
||||
|
|
118
x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts
Normal file
118
x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 uuid from 'uuid';
|
||||
import { merge } from 'lodash';
|
||||
import { makeTls, TlsProps } from './make_tls';
|
||||
|
||||
const INDEX_NAME = 'heartbeat-8-generated-test';
|
||||
|
||||
export const makePing = async (
|
||||
es: any,
|
||||
monitorId: string,
|
||||
fields: { [key: string]: any },
|
||||
mogrify: (doc: any) => any,
|
||||
refresh: boolean = true,
|
||||
tls: boolean | TlsProps = false
|
||||
) => {
|
||||
const baseDoc: any = {
|
||||
tcp: {
|
||||
rtt: {
|
||||
connect: {
|
||||
us: 14687,
|
||||
},
|
||||
},
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'mpls',
|
||||
location: '37.926868, -78.024902',
|
||||
},
|
||||
hostname: 'avc-x1e',
|
||||
},
|
||||
agent: {
|
||||
hostname: 'avc-x1e',
|
||||
id: '10730a1a-4cb7-45ce-8524-80c4820476ab',
|
||||
type: 'heartbeat',
|
||||
ephemeral_id: '0d9a8dc6-f604-49e3-86a0-d8f9d6f2cbad',
|
||||
version: '8.0.0',
|
||||
},
|
||||
'@timestamp': new Date().toISOString(),
|
||||
resolve: {
|
||||
rtt: {
|
||||
us: 350,
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
},
|
||||
ecs: {
|
||||
version: '1.1.0',
|
||||
},
|
||||
host: {
|
||||
name: 'avc-x1e',
|
||||
},
|
||||
http: {
|
||||
rtt: {
|
||||
response_header: {
|
||||
us: 19349,
|
||||
},
|
||||
total: {
|
||||
us: 48954,
|
||||
},
|
||||
write_request: {
|
||||
us: 33,
|
||||
},
|
||||
content: {
|
||||
us: 51,
|
||||
},
|
||||
validate: {
|
||||
us: 19400,
|
||||
},
|
||||
},
|
||||
response: {
|
||||
status_code: 200,
|
||||
body: {
|
||||
bytes: 3,
|
||||
hash: '27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf',
|
||||
},
|
||||
},
|
||||
},
|
||||
monitor: {
|
||||
duration: {
|
||||
us: 49347,
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
id: monitorId,
|
||||
check_group: uuid.v4(),
|
||||
type: 'http',
|
||||
status: 'up',
|
||||
},
|
||||
event: {
|
||||
dataset: 'uptime',
|
||||
},
|
||||
url: {
|
||||
path: '/pattern',
|
||||
scheme: 'http',
|
||||
port: 5678,
|
||||
domain: 'localhost',
|
||||
query: 'r=200x5,500x1',
|
||||
full: 'http://localhost:5678/pattern?r=200x5,500x1',
|
||||
},
|
||||
};
|
||||
|
||||
if (tls) {
|
||||
baseDoc.tls = makeTls(tls as any);
|
||||
}
|
||||
|
||||
const doc = mogrify(merge(baseDoc, fields));
|
||||
|
||||
await es.index({
|
||||
index: INDEX_NAME,
|
||||
refresh,
|
||||
body: doc,
|
||||
});
|
||||
|
||||
return doc;
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export interface TlsProps {
|
||||
valid?: boolean;
|
||||
commonName?: string;
|
||||
expiry?: string;
|
||||
sha256?: string;
|
||||
}
|
||||
|
||||
type Props = TlsProps & boolean;
|
||||
|
||||
// Note This is just a mock sha256 value, this doesn't actually generate actually sha 256 val
|
||||
export const getSha256 = () => {
|
||||
return crypto
|
||||
.randomBytes(64)
|
||||
.toString('hex')
|
||||
.toUpperCase();
|
||||
};
|
||||
|
||||
export const makeTls = ({ valid = true, commonName = '*.elastic.co', expiry, sha256 }: Props) => {
|
||||
const expiryDate =
|
||||
expiry ??
|
||||
moment()
|
||||
.add(valid ? 2 : -2, 'months')
|
||||
.toISOString();
|
||||
|
||||
return {
|
||||
version: '1.3',
|
||||
cipher: 'TLS-AES-128-GCM-SHA256',
|
||||
certificate_not_valid_before: '2020-03-01T00:00:00.000Z',
|
||||
certificate_not_valid_after: expiryDate,
|
||||
server: {
|
||||
x509: {
|
||||
not_before: '2020-03-01T00:00:00.000Z',
|
||||
not_after: '2020-05-30T12:00:00.000Z',
|
||||
issuer: {
|
||||
distinguished_name:
|
||||
'CN=DigiCert SHA2 High Assurance Server CA,OU=www.digicert.com,O=DigiCert Inc,C=US',
|
||||
common_name: 'DigiCert SHA2 High Assurance Server CA',
|
||||
},
|
||||
subject: {
|
||||
common_name: commonName,
|
||||
distinguished_name: 'CN=*.facebook.com,O=Facebook Inc.,L=Menlo Park,ST=California,C=US',
|
||||
},
|
||||
serial_number: '10043199409725537507026285099403602396',
|
||||
signature_algorithm: 'SHA256-RSA',
|
||||
public_key_algorithm: 'ECDSA',
|
||||
public_key_curve: 'P-256',
|
||||
},
|
||||
hash: {
|
||||
sha256: sha256 ?? '1a48f1db13c3bd1482ba1073441e74a1bb1308dc445c88749e0dc4f1889a88a4',
|
||||
sha1: '23291c758d925b9f4bb3584de3763317e94c6ce9',
|
||||
},
|
||||
},
|
||||
established: true,
|
||||
rtt: {
|
||||
handshake: {
|
||||
us: 33103,
|
||||
},
|
||||
},
|
||||
version_protocol: 'tls',
|
||||
};
|
||||
};
|
62
x-pack/test/functional/apps/uptime/certificates.ts
Normal file
62
x-pack/test/functional/apps/uptime/certificates.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks';
|
||||
import { getSha256 } from '../../../api_integration/apis/uptime/rest/helper/make_tls';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const { uptime } = getPageObjects(['uptime']);
|
||||
const uptimeService = getService('uptime');
|
||||
|
||||
const es = getService('es');
|
||||
|
||||
describe('certificate page', function() {
|
||||
before(async () => {
|
||||
await uptime.goToRoot(true);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await makeCheck({ es, tls: true });
|
||||
await uptimeService.navigation.refreshApp();
|
||||
});
|
||||
|
||||
it('can navigate to cert page', async () => {
|
||||
await uptimeService.navigation.refreshApp();
|
||||
await uptimeService.cert.hasViewCertButton();
|
||||
await uptimeService.navigation.goToCertificates();
|
||||
});
|
||||
|
||||
it('displays certificates', async () => {
|
||||
await uptimeService.cert.hasCertificates();
|
||||
});
|
||||
|
||||
it('displays specific certificates', async () => {
|
||||
const certId = getSha256();
|
||||
const { monitorId } = await makeCheck({
|
||||
es,
|
||||
tls: {
|
||||
sha256: certId,
|
||||
},
|
||||
});
|
||||
|
||||
await uptimeService.navigation.refreshApp();
|
||||
await uptimeService.cert.certificateExists({ certId, monitorId });
|
||||
});
|
||||
|
||||
it('performs search against monitor id', async () => {
|
||||
const certId = getSha256();
|
||||
const { monitorId } = await makeCheck({
|
||||
es,
|
||||
tls: {
|
||||
sha256: certId,
|
||||
},
|
||||
});
|
||||
await uptimeService.navigation.refreshApp();
|
||||
await uptimeService.cert.searchIsWorking(monitorId);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -53,6 +53,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
|
|||
|
||||
loadTestFile(require.resolve('./locations'));
|
||||
loadTestFile(require.resolve('./settings'));
|
||||
loadTestFile(require.resolve('./certificates'));
|
||||
});
|
||||
describe('with real-world data', () => {
|
||||
before(async () => {
|
||||
|
|
|
@ -1665,6 +1665,13 @@
|
|||
},
|
||||
"id": {
|
||||
"type": "keyword",
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"norms": false,
|
||||
"analyzer": "simple"
|
||||
}
|
||||
},
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"ip": {
|
||||
|
@ -1672,6 +1679,13 @@
|
|||
},
|
||||
"name": {
|
||||
"type": "keyword",
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"norms": false,
|
||||
"analyzer": "simple"
|
||||
}
|
||||
},
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"status": {
|
||||
|
@ -3079,10 +3093,21 @@
|
|||
},
|
||||
"x509": {
|
||||
"properties": {
|
||||
"alternative_names": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"issuer": {
|
||||
"properties": {
|
||||
"common_name": {
|
||||
"type": "keyword",
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"norms": false,
|
||||
"analyzer": "simple"
|
||||
}
|
||||
},
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"distinguished_name": {
|
||||
|
@ -3092,14 +3117,16 @@
|
|||
}
|
||||
},
|
||||
"not_after": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
"type": "date"
|
||||
},
|
||||
"not_before": {
|
||||
"type": "date"
|
||||
},
|
||||
"public_key_algorithm": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"public_key_algorithm": {
|
||||
"public_key_curve": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
},
|
||||
|
@ -3121,6 +3148,13 @@
|
|||
"properties": {
|
||||
"common_name": {
|
||||
"type": "keyword",
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "text",
|
||||
"norms": false,
|
||||
"analyzer": "simple"
|
||||
}
|
||||
},
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"distinguished_name": {
|
||||
|
@ -3128,6 +3162,10 @@
|
|||
"ignore_above": 1024
|
||||
}
|
||||
}
|
||||
},
|
||||
"version_number": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -13,8 +13,11 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo
|
|||
const retry = getService('retry');
|
||||
|
||||
return new (class UptimePage {
|
||||
public async goToRoot() {
|
||||
public async goToRoot(refresh?: boolean) {
|
||||
await navigation.goToUptime();
|
||||
if (refresh) {
|
||||
await navigation.refreshApp();
|
||||
}
|
||||
}
|
||||
|
||||
public async setDateRange(start: string, end: string) {
|
||||
|
|
50
x-pack/test/functional/services/uptime/certificates.ts
Normal file
50
x-pack/test/functional/services/uptime/certificates.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export function UptimeCertProvider({ getService }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const retry = getService('retry');
|
||||
|
||||
const changeSearchField = async (text: string) => {
|
||||
const input = await testSubjects.find('uptimeCertSearch');
|
||||
await input.clearValueWithKeyboard();
|
||||
await input.type(text);
|
||||
};
|
||||
|
||||
return {
|
||||
async hasViewCertButton() {
|
||||
return retry.tryForTime(15000, async () => {
|
||||
await testSubjects.existOrFail('uptimeCertificatesLink');
|
||||
});
|
||||
},
|
||||
async certificateExists(cert: { certId: string; monitorId: string }) {
|
||||
return retry.tryForTime(15000, async () => {
|
||||
await testSubjects.existOrFail(cert.certId);
|
||||
await testSubjects.existOrFail('monitor-page-link-' + cert.monitorId);
|
||||
});
|
||||
},
|
||||
async hasCertificates(expectedTotal?: number) {
|
||||
return retry.tryForTime(15000, async () => {
|
||||
const totalCerts = await testSubjects.getVisibleText('uptimeCertTotal');
|
||||
if (expectedTotal) {
|
||||
expect(Number(totalCerts) === expectedTotal).to.eql(true);
|
||||
} else {
|
||||
expect(Number(totalCerts) > 0).to.eql(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
async searchIsWorking(monId: string) {
|
||||
const self = this;
|
||||
return retry.tryForTime(15000, async () => {
|
||||
await changeSearchField(monId);
|
||||
await self.hasCertificates(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -25,9 +25,13 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv
|
|||
});
|
||||
};
|
||||
|
||||
const refreshApp = async () => {
|
||||
await testSubjects.click('superDatePickerApplyTimeButton');
|
||||
};
|
||||
|
||||
return {
|
||||
async refreshApp() {
|
||||
await testSubjects.click('superDatePickerApplyTimeButton');
|
||||
await refreshApp();
|
||||
},
|
||||
|
||||
async goToUptime() {
|
||||
|
@ -60,6 +64,13 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv
|
|||
}
|
||||
},
|
||||
|
||||
goToCertificates: async () => {
|
||||
return retry.tryForTime(30 * 1000, async () => {
|
||||
await testSubjects.click('uptimeCertificatesLink');
|
||||
await testSubjects.existOrFail('uptimeCertificatesPage');
|
||||
});
|
||||
},
|
||||
|
||||
async loadDataAndGoToMonitorPage(dateStart: string, dateEnd: string, monitorId: string) {
|
||||
await PageObjects.timePicker.setAbsoluteRange(dateStart, dateEnd);
|
||||
await this.goToMonitor(monitorId);
|
||||
|
|
|
@ -12,6 +12,7 @@ import { UptimeMonitorProvider } from './monitor';
|
|||
import { UptimeNavigationProvider } from './navigation';
|
||||
import { UptimeAlertsProvider } from './alerts';
|
||||
import { UptimeMLAnomalyProvider } from './ml_anomaly';
|
||||
import { UptimeCertProvider } from './certificates';
|
||||
|
||||
export function UptimeProvider(context: FtrProviderContext) {
|
||||
const common = UptimeCommonProvider(context);
|
||||
|
@ -20,6 +21,7 @@ export function UptimeProvider(context: FtrProviderContext) {
|
|||
const navigation = UptimeNavigationProvider(context);
|
||||
const alerts = UptimeAlertsProvider(context);
|
||||
const ml = UptimeMLAnomalyProvider(context);
|
||||
const cert = UptimeCertProvider(context);
|
||||
|
||||
return {
|
||||
common,
|
||||
|
@ -28,5 +30,6 @@ export function UptimeProvider(context: FtrProviderContext) {
|
|||
navigation,
|
||||
alerts,
|
||||
ml,
|
||||
cert,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue