[Uptime] Use async search api for certificates (#111731)

This commit is contained in:
Shahzad 2021-09-15 19:17:33 +02:00 committed by GitHub
parent ce2aac3763
commit 895747ad38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 432 additions and 434 deletions

View file

@ -12,7 +12,7 @@ import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { isCompleteResponse } from '../../../../../src/plugins/data/common'; import { isCompleteResponse } from '../../../../../src/plugins/data/common';
import { useFetcher } from './use_fetcher'; import { useFetcher } from './use_fetcher';
export const useEsSearch = <TParams extends estypes.SearchRequest>( export const useEsSearch = <DocumentSource extends unknown, TParams extends estypes.SearchRequest>(
params: TParams, params: TParams,
fnDeps: any[] fnDeps: any[]
) => { ) => {
@ -43,7 +43,7 @@ export const useEsSearch = <TParams extends estypes.SearchRequest>(
const { rawResponse } = response as any; const { rawResponse } = response as any;
return { data: rawResponse as ESSearchResponse<unknown, TParams>, loading }; return { data: rawResponse as ESSearchResponse<DocumentSource, TParams>, loading };
}; };
export function createEsParams<T extends estypes.SearchRequest>(params: T): T { export function createEsParams<T extends estypes.SearchRequest>(params: T): T {

View file

@ -60,6 +60,7 @@ export {
export const LazyAlertsFlyout = lazy(() => import('./pages/alerts/alerts_flyout')); export const LazyAlertsFlyout = lazy(() => import('./pages/alerts/alerts_flyout'));
export { useFetcher, FETCH_STATUS } from './hooks/use_fetcher'; export { useFetcher, FETCH_STATUS } from './hooks/use_fetcher';
export { useEsSearch, createEsParams } from './hooks/use_es_search';
export * from './typings'; export * from './typings';

View file

@ -6,7 +6,6 @@
*/ */
export enum API_URLS { export enum API_URLS {
CERTS = '/api/uptime/certs',
INDEX_PATTERN = `/api/uptime/index_pattern`, INDEX_PATTERN = `/api/uptime/index_pattern`,
INDEX_STATUS = '/api/uptime/index_status', INDEX_STATUS = '/api/uptime/index_status',
MONITOR_LIST = `/api/uptime/monitor/list`, MONITOR_LIST = `/api/uptime/monitor/list`,

View file

@ -0,0 +1,183 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { estypes } from '@elastic/elasticsearch';
import { CertResult, GetCertsParams, Ping } from '../runtime_types';
import { createEsQuery } from '../utils/es_search';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { CertificatesResults } from '../../server/lib/requests/get_certs';
import { asMutableArray } from '../utils/as_mutable_array';
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 DEFAULT_SORT = 'not_after';
export const DEFAULT_DIRECTION = 'asc';
export const DEFAULT_SIZE = 20;
export const DEFAULT_FROM = 'now-5m';
export const DEFAULT_TO = 'now';
export const getCertsRequestBody = ({
pageIndex,
search,
notValidBefore,
notValidAfter,
size = DEFAULT_SIZE,
to = DEFAULT_TO,
from = DEFAULT_FROM,
sortBy = DEFAULT_SORT,
direction = DEFAULT_DIRECTION,
}: GetCertsParams) => {
const sort = SortFields[sortBy as keyof typeof SortFields];
const searchRequest = createEsQuery({
body: {
from: pageIndex * size,
size,
sort: asMutableArray([
{
[sort]: {
order: direction,
},
},
]),
query: {
bool: {
...(search
? {
minimum_should_match: 1,
should: [
{
multi_match: {
query: escape(search),
type: 'phrase_prefix' as const,
fields: [
'monitor.id.text',
'monitor.name.text',
'url.full.text',
'tls.server.x509.subject.common_name.text',
'tls.server.x509.issuer.common_name.text',
],
},
},
],
}
: {}),
filter: [
{
exists: {
field: 'tls.server.hash.sha256',
},
},
{
range: {
'monitor.timespan': {
gte: from,
lte: to,
},
},
},
...(notValidBefore
? [
{
range: {
'tls.certificate_not_valid_before': {
lte: notValidBefore,
},
},
},
]
: []),
...(notValidAfter
? [
{
range: {
'tls.certificate_not_valid_after': {
lte: notValidAfter,
},
},
},
]
: []),
] as estypes.QueryDslQueryContainer,
},
},
_source: [
'monitor.id',
'monitor.name',
'tls.server.x509.issuer.common_name',
'tls.server.x509.subject.common_name',
'tls.server.hash.sha1',
'tls.server.hash.sha256',
'tls.server.x509.not_after',
'tls.server.x509.not_before',
],
collapse: {
field: 'tls.server.hash.sha256',
inner_hits: {
_source: {
includes: ['monitor.id', 'monitor.name', 'url.full'],
},
collapse: {
field: 'monitor.id',
},
name: 'monitors',
sort: [{ 'monitor.id': 'asc' as const }],
},
},
aggs: {
total: {
cardinality: {
field: 'tls.server.hash.sha256',
},
},
},
},
});
return searchRequest.body;
};
export const processCertsResult = (result: CertificatesResults): CertResult => {
const certs = result.hits?.hits?.map((hit) => {
const ping = hit._source;
const server = ping.tls?.server!;
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) => {
const monitorPing = monitor._source as Ping;
return {
name: monitorPing?.monitor.name,
id: monitorPing?.monitor.id,
url: monitorPing?.url?.full,
};
});
return {
monitors,
issuer,
sha1,
sha256: sha256 as string,
not_after: notAfter,
not_before: notBefore,
common_name: commonName,
};
});
const total = result.aggregations?.total?.value ?? 0;
return { certs, total };
};

View file

@ -9,10 +9,7 @@ import * as t from 'io-ts';
export const GetCertsParamsType = t.intersection([ export const GetCertsParamsType = t.intersection([
t.type({ t.type({
index: t.number, pageIndex: t.number,
size: t.number,
sortBy: t.string,
direction: t.string,
}), }),
t.partial({ t.partial({
search: t.string, search: t.string,
@ -20,6 +17,9 @@ export const GetCertsParamsType = t.intersection([
notValidAfter: t.string, notValidAfter: t.string,
from: t.string, from: t.string,
to: t.string, to: t.string,
sortBy: t.string,
direction: t.string,
size: t.number,
}), }),
]); ]);

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { estypes } from '@elastic/elasticsearch';
export function createEsQuery<T extends estypes.SearchRequest>(params: T): T {
return params;
}

View file

@ -1,105 +0,0 @@
// 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,
}
}
>
<ContextProvider
value={
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],
}
}
>
<CertificateList
onChange={[MockFunction]}
page={
Object {
"index": 0,
"size": 10,
}
}
sort={
Object {
"direction": "asc",
"field": "not_after",
}
}
/>
</ContextProvider>
</ContextProvider>
`;

View file

@ -5,9 +5,10 @@
* 2.0. * 2.0.
*/ */
import React, { ChangeEvent } from 'react'; import React, { ChangeEvent, useState } from 'react';
import { EuiFieldSearch } from '@elastic/eui'; import { EuiFieldSearch } from '@elastic/eui';
import styled from 'styled-components'; import styled from 'styled-components';
import useDebounce from 'react-use/lib/useDebounce';
import * as labels from './translations'; import * as labels from './translations';
const WrapFieldSearch = styled('div')` const WrapFieldSearch = styled('div')`
@ -19,10 +20,20 @@ interface Props {
} }
export const CertificateSearch: React.FC<Props> = ({ setSearch }) => { export const CertificateSearch: React.FC<Props> = ({ setSearch }) => {
const [debouncedValue, setDebouncedValue] = useState('');
const onChange = (e: ChangeEvent<HTMLInputElement>) => { const onChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value); setDebouncedValue(e.target.value);
}; };
useDebounce(
() => {
setSearch(debouncedValue);
},
350,
[debouncedValue]
);
return ( return (
<WrapFieldSearch> <WrapFieldSearch>
<EuiFieldSearch <EuiFieldSearch

View file

@ -11,14 +11,14 @@ import { useSelector } from 'react-redux';
import { certificatesSelector } from '../../state/certificates/certificates'; import { certificatesSelector } from '../../state/certificates/certificates';
export const CertificateTitle = () => { export const CertificateTitle = () => {
const { data: certificates } = useSelector(certificatesSelector); const total = useSelector(certificatesSelector);
return ( return (
<FormattedMessage <FormattedMessage
id="xpack.uptime.certificates.heading" id="xpack.uptime.certificates.heading"
defaultMessage="TLS Certificates ({total})" defaultMessage="TLS Certificates ({total})"
values={{ values={{
total: <span data-test-subj="uptimeCertTotal">{certificates?.total ?? 0}</span>, total: <span data-test-subj="uptimeCertTotal">{total ?? 0}</span>,
}} }}
/> />
); );

View file

@ -6,11 +6,11 @@
*/ */
import React from 'react'; import React from 'react';
import { shallowWithRouter } from '../../lib';
import { CertificateList, CertSort } from './certificates_list'; import { CertificateList, CertSort } from './certificates_list';
import { render } from '../../lib/helper/rtl_helpers';
describe('CertificateList', () => { describe('CertificateList', () => {
it('shallow renders expected elements for valid props', () => { it('render empty state', () => {
const page = { const page = {
index: 0, index: 0,
size: 10, size: 10,
@ -20,8 +20,59 @@ describe('CertificateList', () => {
direction: 'asc', direction: 'asc',
}; };
const { getByText } = render(
<CertificateList
page={page}
sort={sort}
onChange={jest.fn()}
certificates={{ loading: false, total: 0, certs: [] }}
/>
);
expect( expect(
shallowWithRouter(<CertificateList page={page} sort={sort} onChange={jest.fn()} />) getByText('No Certificates found. Note: Certificates are only visible for Heartbeat 7.8+')
).toMatchSnapshot(); ).toBeInTheDocument();
});
it('renders certificates list', () => {
const page = {
index: 0,
size: 10,
};
const sort: CertSort = {
field: 'not_after',
direction: 'asc',
};
const { getByText } = render(
<CertificateList
page={page}
sort={sort}
onChange={jest.fn()}
certificates={{
loading: false,
total: 1,
certs: [
{
monitors: [
{
name: 'BadSSL Expired',
id: 'expired-badssl',
url: 'https://expired.badssl.com/',
},
],
issuer: 'COMODO RSA Domain Validation Secure Server CA',
sha1: '404bbd2f1f4cc2fdeef13aabdd523ef61f1c71f3',
sha256: 'ba105ce02bac76888ecee47cd4eb7941653e9ac993b61b2eb3dcc82014d21b4f',
not_after: '2015-04-12T23:59:59.000Z',
not_before: '2015-04-09T00:00:00.000Z',
common_name: '*.badssl.com',
},
],
}}
/>
);
expect(getByText('BadSSL Expired')).toBeInTheDocument();
}); });
}); });

View file

@ -7,13 +7,11 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import { useSelector } from 'react-redux';
import { Direction, EuiBasicTable } from '@elastic/eui'; import { Direction, EuiBasicTable } from '@elastic/eui';
import { certificatesSelector } from '../../state/certificates/certificates';
import { CertStatus } from './cert_status'; import { CertStatus } from './cert_status';
import { CertMonitors } from './cert_monitors'; import { CertMonitors } from './cert_monitors';
import * as labels from './translations'; import * as labels from './translations';
import { Cert, CertMonitor } from '../../../common/runtime_types'; import { Cert, CertMonitor, CertResult } from '../../../common/runtime_types';
import { FingerprintCol } from './fingerprint_col'; import { FingerprintCol } from './fingerprint_col';
import { LOADING_CERTIFICATES, NO_CERTS_AVAILABLE } from './translations'; import { LOADING_CERTIFICATES, NO_CERTS_AVAILABLE } from './translations';
@ -40,11 +38,10 @@ interface Props {
page: Page; page: Page;
sort: CertSort; sort: CertSort;
onChange: (page: Page, sort: CertSort) => void; onChange: (page: Page, sort: CertSort) => void;
certificates: CertResult & { loading?: boolean };
} }
export const CertificateList: React.FC<Props> = ({ page, sort, onChange }) => { export const CertificateList: React.FC<Props> = ({ page, certificates, sort, onChange }) => {
const { data: certificates, loading } = useSelector(certificatesSelector);
const onTableChange = (newVal: Partial<Props>) => { const onTableChange = (newVal: Partial<Props>) => {
onChange(newVal.page as Page, newVal.sort as CertSort); onChange(newVal.page as Page, newVal.sort as CertSort);
}; };
@ -100,7 +97,7 @@ export const CertificateList: React.FC<Props> = ({ page, sort, onChange }) => {
return ( return (
<EuiBasicTable <EuiBasicTable
loading={loading} loading={certificates.loading}
columns={columns} columns={columns}
items={certificates?.certs ?? []} items={certificates?.certs ?? []}
pagination={pagination} pagination={pagination}
@ -112,7 +109,7 @@ export const CertificateList: React.FC<Props> = ({ page, sort, onChange }) => {
}, },
}} }}
noItemsMessage={ noItemsMessage={
loading ? ( certificates.loading ? (
LOADING_CERTIFICATES LOADING_CERTIFICATES
) : ( ) : (
<span data-test-subj="uptimeCertsEmptyMessage">{NO_CERTS_AVAILABLE}</span> <span data-test-subj="uptimeCertsEmptyMessage">{NO_CERTS_AVAILABLE}</span>

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useSelector } from 'react-redux';
import { useContext } from 'react';
import { useEsSearch, createEsParams } from '../../../../observability/public';
import { CertResult, GetCertsParams, Ping } from '../../../common/runtime_types';
import { selectDynamicSettings } from '../../state/selectors';
import {
DEFAULT_DIRECTION,
DEFAULT_FROM,
DEFAULT_SIZE,
DEFAULT_SORT,
DEFAULT_TO,
getCertsRequestBody,
processCertsResult,
} from '../../../common/requests/get_certs_request_body';
import { UptimeRefreshContext } from '../../contexts';
export const useCertSearch = ({
pageIndex,
size = DEFAULT_SIZE,
search,
sortBy = DEFAULT_SORT,
direction = DEFAULT_DIRECTION,
}: GetCertsParams): CertResult & { loading?: boolean } => {
const settings = useSelector(selectDynamicSettings);
const { lastRefresh } = useContext(UptimeRefreshContext);
const searchBody = getCertsRequestBody({
pageIndex,
size,
search,
sortBy,
direction,
to: DEFAULT_TO,
from: DEFAULT_FROM,
});
const esParams = createEsParams({
index: settings.settings?.heartbeatIndices,
body: searchBody,
});
const { data: result, loading } = useEsSearch<Ping, typeof esParams>(esParams, [
settings.settings?.heartbeatIndices,
size,
pageIndex,
lastRefresh,
search,
]);
return result ? { ...processCertsResult(result), loading } : { certs: [], total: 0, loading };
};

View file

@ -95,10 +95,7 @@ export const mockState: AppState = {
}, },
}, },
certificates: { certificates: {
certs: { total: 0,
data: null,
loading: false,
},
}, },
selectedFilters: null, selectedFilters: null,
alerts: { alerts: {

View file

@ -7,13 +7,13 @@
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { EuiSpacer } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui';
import React, { useContext, useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTrackPageview } from '../../../observability/public'; import { useTrackPageview } from '../../../observability/public';
import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
import { getDynamicSettings } from '../state/actions/dynamic_settings'; import { getDynamicSettings } from '../state/actions/dynamic_settings';
import { UptimeRefreshContext } from '../contexts';
import { getCertificatesAction } from '../state/certificates/certificates';
import { CertificateList, CertificateSearch, CertSort } from '../components/certificates'; import { CertificateList, CertificateSearch, CertSort } from '../components/certificates';
import { useCertSearch } from '../components/certificates/use_cert_search';
import { setCertificatesTotalAction } from '../state/certificates/certificates';
const DEFAULT_PAGE_SIZE = 10; const DEFAULT_PAGE_SIZE = 10;
const LOCAL_STORAGE_KEY = 'xpack.uptime.certList.pageSize'; const LOCAL_STORAGE_KEY = 'xpack.uptime.certList.pageSize';
@ -40,22 +40,21 @@ export const CertificatesPage: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { lastRefresh } = useContext(UptimeRefreshContext);
useEffect(() => { useEffect(() => {
dispatch(getDynamicSettings()); dispatch(getDynamicSettings());
}, [dispatch]); }, [dispatch]);
const certificates = useCertSearch({
search,
size: page.size,
pageIndex: page.index,
sortBy: sort.field,
direction: sort.direction,
});
useEffect(() => { useEffect(() => {
dispatch( dispatch(setCertificatesTotalAction({ total: certificates.total }));
getCertificatesAction.get({ }, [certificates.total, dispatch]);
search,
...page,
sortBy: sort.field,
direction: sort.direction,
})
);
}, [dispatch, page, search, sort.direction, sort.field, lastRefresh]);
return ( return (
<> <>
@ -70,6 +69,7 @@ export const CertificatesPage: React.FC = () => {
localStorage.setItem(LOCAL_STORAGE_KEY, pageVal.size.toString()); localStorage.setItem(LOCAL_STORAGE_KEY, pageVal.size.toString());
}} }}
sort={sort} sort={sort}
certificates={certificates}
/> />
</> </>
); );

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { 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);
};

View file

@ -5,40 +5,27 @@
* 2.0. * 2.0.
*/ */
import { handleActions } from 'redux-actions'; import { Action, createAction, handleActions } from 'redux-actions';
import { takeLatest } from 'redux-saga/effects';
import { createAsyncAction } from '../actions/utils';
import { asyncInitState, handleAsyncAction } from '../reducers/utils';
import { CertResult, GetCertsParams } from '../../../common/runtime_types';
import { AppState } from '../index'; import { AppState } from '../index';
import { AsyncInitState } from '../reducers/types';
import { fetchEffectFactory } from '../effects/fetch_effect';
import { fetchCertificates } from '../api/certificates';
export const getCertificatesAction = createAsyncAction<GetCertsParams, CertResult>( export const setCertificatesTotalAction = createAction<CertificatesState>('SET_CERTIFICATES_TOTAL');
'GET_CERTIFICATES'
);
export interface CertificatesState { export interface CertificatesState {
certs: AsyncInitState<CertResult>; total: number;
} }
const initialState = { const initialState = {
certs: asyncInitState(), total: 0,
}; };
export const certificatesReducer = handleActions<CertificatesState>( export const certificatesReducer = handleActions<CertificatesState>(
{ {
...handleAsyncAction<CertificatesState>('certs', getCertificatesAction), [String(setCertificatesTotalAction)]: (state, action: Action<CertificatesState>) => ({
...state,
total: action.payload.total,
}),
}, },
initialState initialState
); );
export function* fetchCertificatesEffect() { export const certificatesSelector = ({ certificates }: AppState) => certificates.total;
yield takeLatest(
getCertificatesAction.get,
fetchEffectFactory(fetchCertificates, getCertificatesAction.success, getCertificatesAction.fail)
);
}
export const certificatesSelector = ({ certificates }: AppState) => certificates.certs;

View file

@ -16,7 +16,6 @@ import { fetchPingsEffect, fetchPingHistogramEffect } from './ping';
import { fetchMonitorDurationEffect } from './monitor_duration'; import { fetchMonitorDurationEffect } from './monitor_duration';
import { fetchMLJobEffect } from './ml_anomaly'; import { fetchMLJobEffect } from './ml_anomaly';
import { fetchIndexStatusEffect } from './index_status'; import { fetchIndexStatusEffect } from './index_status';
import { fetchCertificatesEffect } from '../certificates/certificates';
import { fetchAlertsEffect } from '../alerts/alerts'; import { fetchAlertsEffect } from '../alerts/alerts';
import { fetchJourneyStepsEffect } from './journey'; import { fetchJourneyStepsEffect } from './journey';
import { fetchNetworkEventsEffect } from './network_events'; import { fetchNetworkEventsEffect } from './network_events';
@ -39,7 +38,6 @@ export function* rootEffect() {
yield fork(fetchMLJobEffect); yield fork(fetchMLJobEffect);
yield fork(fetchMonitorDurationEffect); yield fork(fetchMonitorDurationEffect);
yield fork(fetchIndexStatusEffect); yield fork(fetchIndexStatusEffect);
yield fork(fetchCertificatesEffect);
yield fork(fetchAlertsEffect); yield fork(fetchAlertsEffect);
yield fork(fetchJourneyStepsEffect); yield fork(fetchJourneyStepsEffect);
yield fork(fetchNetworkEventsEffect); yield fork(fetchNetworkEventsEffect);

View file

@ -93,10 +93,7 @@ describe('state selectors', () => {
}, },
}, },
certificates: { certificates: {
certs: { total: 0,
data: null,
loading: false,
},
}, },
selectedFilters: null, selectedFilters: null,
alerts: { alerts: {

View file

@ -6,11 +6,10 @@
*/ */
import moment from 'moment'; import moment from 'moment';
import { ALERT_SEVERITY_WARNING, ALERT_SEVERITY } from '@kbn/rule-data-utils'; import { ALERT_SEVERITY_WARNING, ALERT_SEVERITY } from '@kbn/rule-data-utils';
import { tlsAlertFactory, getCertSummary, DEFAULT_SIZE } from './tls'; import { tlsAlertFactory, getCertSummary } from './tls';
import { TLS } from '../../../common/constants/alerts'; import { TLS } from '../../../common/constants/alerts';
import { CertResult, DynamicSettings } from '../../../common/runtime_types'; import { CertResult, DynamicSettings } from '../../../common/runtime_types';
import { createRuleTypeMocks, bootstrapDependencies } from './test_utils'; import { createRuleTypeMocks, bootstrapDependencies } from './test_utils';
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { savedObjectsAdapter, UMSavedObjectsAdapter } from '../saved_objects'; import { savedObjectsAdapter, UMSavedObjectsAdapter } from '../saved_objects';
@ -123,10 +122,8 @@ describe('tls alert', () => {
}); });
expect(mockGetter).toBeCalledWith( expect(mockGetter).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
from: DEFAULT_FROM, pageIndex: 0,
to: DEFAULT_TO, size: 1000,
index: 0,
size: DEFAULT_SIZE,
notValidAfter: `now+${DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold}d`, notValidAfter: `now+${DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold}d`,
notValidBefore: `now-${DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold}d`, notValidBefore: `now-${DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold}d`,
sortBy: 'common_name', sortBy: 'common_name',

View file

@ -13,7 +13,6 @@ import { TLS } from '../../../common/constants/alerts';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { Cert, CertResult } from '../../../common/runtime_types'; import { Cert, CertResult } from '../../../common/runtime_types';
import { commonStateTranslations, tlsTranslations } from './translations'; import { commonStateTranslations, tlsTranslations } from './translations';
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
import { TlsTranslations } from '../../../common/translations'; import { TlsTranslations } from '../../../common/translations';
import { ActionGroupIdsOf } from '../../../../alerting/common'; import { ActionGroupIdsOf } from '../../../../alerting/common';
@ -23,8 +22,6 @@ import { createUptimeESClient } from '../lib';
export type ActionGroupIds = ActionGroupIdsOf<typeof TLS>; export type ActionGroupIds = ActionGroupIdsOf<typeof TLS>;
export const DEFAULT_SIZE = 20;
interface TlsAlertState { interface TlsAlertState {
commonName: string; commonName: string;
issuer: string; issuer: string;
@ -130,10 +127,8 @@ export const tlsAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server,
const { certs, total }: CertResult = await libs.requests.getCerts({ const { certs, total }: CertResult = await libs.requests.getCerts({
uptimeEsClient, uptimeEsClient,
from: DEFAULT_FROM, pageIndex: 0,
to: DEFAULT_TO, size: 1000,
index: 0,
size: DEFAULT_SIZE,
notValidAfter: `now+${ notValidAfter: `now+${
dynamicSettings?.certExpirationThreshold ?? dynamicSettings?.certExpirationThreshold ??
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold

View file

@ -13,7 +13,6 @@ import { TLS_LEGACY } from '../../../common/constants/alerts';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
import { Cert, CertResult } from '../../../common/runtime_types'; import { Cert, CertResult } from '../../../common/runtime_types';
import { commonStateTranslations, tlsTranslations } from './translations'; import { commonStateTranslations, tlsTranslations } from './translations';
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
import { ActionGroupIdsOf } from '../../../../alerting/common'; import { ActionGroupIdsOf } from '../../../../alerting/common';
import { AlertInstanceContext } from '../../../../alerting/common'; import { AlertInstanceContext } from '../../../../alerting/common';
@ -21,13 +20,16 @@ import { AlertInstance } from '../../../../alerting/server';
import { savedObjectsAdapter } from '../saved_objects'; import { savedObjectsAdapter } from '../saved_objects';
import { createUptimeESClient } from '../lib'; import { createUptimeESClient } from '../lib';
import {
DEFAULT_FROM,
DEFAULT_SIZE,
DEFAULT_TO,
} from '../../../common/requests/get_certs_request_body';
export type ActionGroupIds = ActionGroupIdsOf<typeof TLS_LEGACY>; export type ActionGroupIds = ActionGroupIdsOf<typeof TLS_LEGACY>;
type TLSAlertInstance = AlertInstance<Record<string, any>, AlertInstanceContext, ActionGroupIds>; type TLSAlertInstance = AlertInstance<Record<string, any>, AlertInstanceContext, ActionGroupIds>;
const DEFAULT_SIZE = 20;
interface TlsAlertState { interface TlsAlertState {
count: number; count: number;
agingCount: number; agingCount: number;
@ -125,7 +127,7 @@ export const tlsLegacyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_s
uptimeEsClient, uptimeEsClient,
from: DEFAULT_FROM, from: DEFAULT_FROM,
to: DEFAULT_TO, to: DEFAULT_TO,
index: 0, pageIndex: 0,
size: DEFAULT_SIZE, size: DEFAULT_SIZE,
notValidAfter: `now+${ notValidAfter: `now+${
dynamicSettings?.certExpirationThreshold ?? dynamicSettings?.certExpirationThreshold ??

View file

@ -58,9 +58,9 @@ export function createUptimeESClient({
return { return {
baseESClient: esClient, baseESClient: esClient,
async search<TParams extends estypes.SearchRequest>( async search<DocumentSource extends unknown, TParams extends estypes.SearchRequest>(
params: TParams params: TParams
): Promise<{ body: ESSearchResponse<unknown, TParams> }> { ): Promise<{ body: ESSearchResponse<DocumentSource, TParams> }> {
let res: any; let res: any;
let esError: any; let esError: any;
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(
@ -155,7 +155,3 @@ export function debugESCall({
} }
console.log(`\n`); console.log(`\n`);
} }
export function createEsQuery<T extends estypes.SearchRequest>(params: T): T {
return params;
}

View file

@ -94,7 +94,7 @@ describe('getCerts', () => {
const result = await getCerts({ const result = await getCerts({
uptimeEsClient, uptimeEsClient,
index: 1, pageIndex: 1,
from: 'now-2d', from: 'now-2d',
to: 'now+1h', to: 'now+1h',
search: 'my_common_name', search: 'my_common_name',

View file

@ -5,170 +5,37 @@
* 2.0. * 2.0.
*/ */
import { PromiseType } from 'utility-types';
import { UMElasticsearchQueryFn } from '../adapters'; import { UMElasticsearchQueryFn } from '../adapters';
import { CertResult, GetCertsParams, Ping } from '../../../common/runtime_types'; import { CertResult, GetCertsParams, Ping } from '../../../common/runtime_types';
import {
getCertsRequestBody,
processCertsResult,
} from '../../../common/requests/get_certs_request_body';
import { UptimeESClient } from '../lib';
enum SortFields { export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = async (
'issuer' = 'tls.server.x509.issuer.common_name', requestParams
'not_after' = 'tls.server.x509.not_after', ) => {
'not_before' = 'tls.server.x509.not_before', const result = await getCertsResults(requestParams);
'common_name' = 'tls.server.x509.subject.common_name',
}
export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = async ({ return processCertsResult(result);
uptimeEsClient, };
index,
from,
to,
size,
search,
notValidBefore,
notValidAfter,
sortBy,
direction,
}) => {
const sort = SortFields[sortBy as keyof typeof SortFields];
const searchBody = { export type CertificatesResults = PromiseType<ReturnType<typeof getCertsResults>>;
from: index * size,
size,
sort: [
{
[sort]: {
order: direction as 'asc' | 'desc',
},
},
],
query: {
bool: {
...(search
? {
minimum_should_match: 1,
should: [
{
multi_match: {
query: escape(search),
type: 'phrase_prefix' as const,
fields: [
'monitor.id.text',
'monitor.name.text',
'url.full.text',
'tls.server.x509.subject.common_name.text',
'tls.server.x509.issuer.common_name.text',
],
},
},
],
}
: {}),
filter: [
{
exists: {
field: 'tls.server.hash.sha256',
},
},
{
range: {
'monitor.timespan': {
gte: from,
lte: to,
},
},
},
],
},
},
_source: [
'monitor.id',
'monitor.name',
'tls.server.x509.issuer.common_name',
'tls.server.x509.subject.common_name',
'tls.server.hash.sha1',
'tls.server.hash.sha256',
'tls.server.x509.not_after',
'tls.server.x509.not_before',
],
collapse: {
field: 'tls.server.hash.sha256',
inner_hits: {
_source: {
includes: ['monitor.id', 'monitor.name', 'url.full'],
},
collapse: {
field: 'monitor.id',
},
name: 'monitors',
sort: [{ 'monitor.id': 'asc' as const }],
},
},
aggs: {
total: {
cardinality: {
field: 'tls.server.hash.sha256',
},
},
},
};
if (notValidBefore || notValidAfter) { const getCertsResults = async (
const validityFilters: any = { requestParams: GetCertsParams & { uptimeEsClient: UptimeESClient }
bool: { ) => {
should: [], const { uptimeEsClient } = requestParams;
},
};
if (notValidBefore) {
validityFilters.bool.should.push({
range: {
'tls.certificate_not_valid_before': {
lte: notValidBefore,
},
},
});
}
if (notValidAfter) {
validityFilters.bool.should.push({
range: {
'tls.certificate_not_valid_after': {
lte: notValidAfter,
},
},
});
}
searchBody.query.bool.filter.push(validityFilters); const searchBody = getCertsRequestBody(requestParams);
}
const { body: result } = await uptimeEsClient.search({ const request = { body: searchBody };
const { body: result } = await uptimeEsClient.search<Ping, typeof request>({
body: searchBody, body: searchBody,
}); });
const certs = (result?.hits?.hits ?? []).map((hit) => { return result;
const ping = hit._source as Ping;
const server = ping.tls?.server!;
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,
issuer,
sha1,
sha256: sha256 as string,
not_after: notAfter,
not_before: notBefore,
common_name: commonName,
};
});
const total = result?.aggregations?.total?.value ?? 0;
return { certs, total };
}; };

View file

@ -10,7 +10,7 @@ import { GetPingHistogramParams, HistogramResult } from '../../../common/runtime
import { QUERY } from '../../../common/constants'; import { QUERY } from '../../../common/constants';
import { getHistogramInterval } from '../helper/get_histogram_interval'; import { getHistogramInterval } from '../helper/get_histogram_interval';
import { UMElasticsearchQueryFn } from '../adapters/framework'; import { UMElasticsearchQueryFn } from '../adapters/framework';
import { createEsQuery } from '../lib'; import { createEsQuery } from '../../../common/utils/es_search';
export const getPingHistogram: UMElasticsearchQueryFn< export const getPingHistogram: UMElasticsearchQueryFn<
GetPingHistogramParams, GetPingHistogramParams,

View file

@ -1,54 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { API_URLS } from '../../../common/constants';
import { UMServerLibs } from '../../lib/lib';
import { UMRestApiRouteFactory } from '../types';
export const DEFAULT_FROM = 'now-5m';
export const DEFAULT_TO = 'now';
const DEFAULT_SIZE = 25;
const DEFAULT_SORT = 'not_after';
const DEFAULT_DIRECTION = 'asc';
export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
method: 'GET',
path: API_URLS.CERTS,
validate: {
query: schema.object({
from: schema.maybe(schema.string()),
to: schema.maybe(schema.string()),
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()),
}),
},
handler: async ({ uptimeEsClient, request }): Promise<any> => {
const index = request.query?.index ?? 0;
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;
return await libs.requests.getCerts({
uptimeEsClient,
index,
search,
size,
from,
to,
sortBy,
direction,
});
},
});

View file

@ -5,7 +5,6 @@
* 2.0. * 2.0.
*/ */
import { createGetCertsRoute } from './certs/certs';
import { createGetOverviewFilters } from './overview_filters'; import { createGetOverviewFilters } from './overview_filters';
import { import {
createGetPingHistogramRoute, createGetPingHistogramRoute,
@ -35,7 +34,6 @@ export { createRouteWithAuth } from './create_route_with_auth';
export { uptimeRouteWrapper } from './uptime_route_wrapper'; export { uptimeRouteWrapper } from './uptime_route_wrapper';
export const restApiRoutes: UMRestApiRouteFactory[] = [ export const restApiRoutes: UMRestApiRouteFactory[] = [
createGetCertsRoute,
createGetOverviewFilters, createGetOverviewFilters,
createGetPingsRoute, createGetPingsRoute,
createGetIndexPatternRoute, createGetIndexPatternRoute,

View file

@ -9,9 +9,12 @@ import expect from '@kbn/expect';
import moment from 'moment'; import moment from 'moment';
import { isRight } from 'fp-ts/lib/Either'; import { isRight } from 'fp-ts/lib/Either';
import { FtrProviderContext } from '../../../ftr_provider_context'; import { FtrProviderContext } from '../../../ftr_provider_context';
import { API_URLS } from '../../../../../plugins/uptime/common/constants';
import { CertType } from '../../../../../plugins/uptime/common/runtime_types'; import { CertType } from '../../../../../plugins/uptime/common/runtime_types';
import { makeChecksWithStatus } from './helper/make_checks'; import { makeChecksWithStatus } from './helper/make_checks';
import {
processCertsResult,
getCertsRequestBody,
} from '../../../../../plugins/uptime/common/requests/get_certs_request_body';
export default function ({ getService }: FtrProviderContext) { export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest'); const supertest = getService('supertest');
@ -21,8 +24,18 @@ export default function ({ getService }: FtrProviderContext) {
describe('certs api', () => { describe('certs api', () => {
describe('empty index', async () => { describe('empty index', async () => {
it('returns empty array for no data', async () => { it('returns empty array for no data', async () => {
const apiResponse = await supertest.get(API_URLS.CERTS); const apiResponse = await supertest
expect(JSON.stringify(apiResponse.body)).to.eql('{"certs":[],"total":0}'); .post(`/internal/search/ese`)
.set('kbn-xsrf', 'true')
.send({
params: {
index: 'heartbeat-*',
body: getCertsRequestBody({ pageIndex: 0, size: 10 }),
},
});
const result = processCertsResult(apiResponse.body.rawResponse);
expect(JSON.stringify(result)).to.eql('{"certs":[],"total":0}');
}); });
}); });
@ -67,19 +80,29 @@ export default function ({ getService }: FtrProviderContext) {
}); });
it('retrieves expected cert data', async () => { it('retrieves expected cert data', async () => {
const apiResponse = await supertest.get(API_URLS.CERTS); const { body } = await supertest
const { body } = apiResponse; .post(`/internal/search/ese`)
.set('kbn-xsrf', 'true')
.send({
params: {
index: 'heartbeat-*',
body: getCertsRequestBody({ pageIndex: 0, size: 10 }),
},
});
expect(body.certs).not.to.be(undefined); const result = processCertsResult(body.rawResponse);
expect(Array.isArray(body.certs)).to.be(true);
expect(body.certs).to.have.length(1);
const decoded = CertType.decode(body.certs[0]); expect(result.certs).not.to.be(undefined);
expect(Array.isArray(result.certs)).to.be(true);
expect(result.certs).to.have.length(1);
const decoded = CertType.decode(result.certs[0]);
expect(isRight(decoded)).to.be(true); expect(isRight(decoded)).to.be(true);
const cert = body.certs[0]; const cert = result.certs[0];
expect(Array.isArray(cert.monitors)).to.be(true); expect(Array.isArray(cert.monitors)).to.be(true);
expect(cert.monitors[0]).to.eql({ expect(cert.monitors[0]).to.eql({
name: undefined,
id: monitorId, id: monitorId,
url: 'http://localhost:5678/pattern?r=200x5,500x1', url: 'http://localhost:5678/pattern?r=200x5,500x1',
}); });