[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 { useFetcher } from './use_fetcher';
export const useEsSearch = <TParams extends estypes.SearchRequest>(
export const useEsSearch = <DocumentSource extends unknown, TParams extends estypes.SearchRequest>(
params: TParams,
fnDeps: any[]
) => {
@ -43,7 +43,7 @@ export const useEsSearch = <TParams extends estypes.SearchRequest>(
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 {

View file

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

View file

@ -6,7 +6,6 @@
*/
export enum API_URLS {
CERTS = '/api/uptime/certs',
INDEX_PATTERN = `/api/uptime/index_pattern`,
INDEX_STATUS = '/api/uptime/index_status',
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([
t.type({
index: t.number,
size: t.number,
sortBy: t.string,
direction: t.string,
pageIndex: t.number,
}),
t.partial({
search: t.string,
@ -20,6 +17,9 @@ export const GetCertsParamsType = t.intersection([
notValidAfter: t.string,
from: 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.
*/
import React, { ChangeEvent } from 'react';
import React, { ChangeEvent, useState } from 'react';
import { EuiFieldSearch } from '@elastic/eui';
import styled from 'styled-components';
import useDebounce from 'react-use/lib/useDebounce';
import * as labels from './translations';
const WrapFieldSearch = styled('div')`
@ -19,10 +20,20 @@ interface Props {
}
export const CertificateSearch: React.FC<Props> = ({ setSearch }) => {
const [debouncedValue, setDebouncedValue] = useState('');
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
setDebouncedValue(e.target.value);
};
useDebounce(
() => {
setSearch(debouncedValue);
},
350,
[debouncedValue]
);
return (
<WrapFieldSearch>
<EuiFieldSearch

View file

@ -11,14 +11,14 @@ import { useSelector } from 'react-redux';
import { certificatesSelector } from '../../state/certificates/certificates';
export const CertificateTitle = () => {
const { data: certificates } = useSelector(certificatesSelector);
const total = useSelector(certificatesSelector);
return (
<FormattedMessage
id="xpack.uptime.certificates.heading"
defaultMessage="TLS Certificates ({total})"
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 { shallowWithRouter } from '../../lib';
import { CertificateList, CertSort } from './certificates_list';
import { render } from '../../lib/helper/rtl_helpers';
describe('CertificateList', () => {
it('shallow renders expected elements for valid props', () => {
it('render empty state', () => {
const page = {
index: 0,
size: 10,
@ -20,8 +20,59 @@ describe('CertificateList', () => {
direction: 'asc',
};
const { getByText } = render(
<CertificateList
page={page}
sort={sort}
onChange={jest.fn()}
certificates={{ loading: false, total: 0, certs: [] }}
/>
);
expect(
shallowWithRouter(<CertificateList page={page} sort={sort} onChange={jest.fn()} />)
).toMatchSnapshot();
getByText('No Certificates found. Note: Certificates are only visible for Heartbeat 7.8+')
).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 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 { Cert, CertMonitor, CertResult } from '../../../common/runtime_types';
import { FingerprintCol } from './fingerprint_col';
import { LOADING_CERTIFICATES, NO_CERTS_AVAILABLE } from './translations';
@ -40,11 +38,10 @@ interface Props {
page: Page;
sort: CertSort;
onChange: (page: Page, sort: CertSort) => void;
certificates: CertResult & { loading?: boolean };
}
export const CertificateList: React.FC<Props> = ({ page, sort, onChange }) => {
const { data: certificates, loading } = useSelector(certificatesSelector);
export const CertificateList: React.FC<Props> = ({ page, certificates, sort, onChange }) => {
const onTableChange = (newVal: Partial<Props>) => {
onChange(newVal.page as Page, newVal.sort as CertSort);
};
@ -100,7 +97,7 @@ export const CertificateList: React.FC<Props> = ({ page, sort, onChange }) => {
return (
<EuiBasicTable
loading={loading}
loading={certificates.loading}
columns={columns}
items={certificates?.certs ?? []}
pagination={pagination}
@ -112,7 +109,7 @@ export const CertificateList: React.FC<Props> = ({ page, sort, onChange }) => {
},
}}
noItemsMessage={
loading ? (
certificates.loading ? (
LOADING_CERTIFICATES
) : (
<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: {
certs: {
data: null,
loading: false,
},
total: 0,
},
selectedFilters: null,
alerts: {

View file

@ -7,13 +7,13 @@
import { useDispatch } from 'react-redux';
import { EuiSpacer } from '@elastic/eui';
import React, { useContext, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useTrackPageview } from '../../../observability/public';
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
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 { useCertSearch } from '../components/certificates/use_cert_search';
import { setCertificatesTotalAction } from '../state/certificates/certificates';
const DEFAULT_PAGE_SIZE = 10;
const LOCAL_STORAGE_KEY = 'xpack.uptime.certList.pageSize';
@ -40,22 +40,21 @@ export const CertificatesPage: React.FC = () => {
const dispatch = useDispatch();
const { lastRefresh } = useContext(UptimeRefreshContext);
useEffect(() => {
dispatch(getDynamicSettings());
}, [dispatch]);
const certificates = useCertSearch({
search,
size: page.size,
pageIndex: page.index,
sortBy: sort.field,
direction: sort.direction,
});
useEffect(() => {
dispatch(
getCertificatesAction.get({
search,
...page,
sortBy: sort.field,
direction: sort.direction,
})
);
}, [dispatch, page, search, sort.direction, sort.field, lastRefresh]);
dispatch(setCertificatesTotalAction({ total: certificates.total }));
}, [certificates.total, dispatch]);
return (
<>
@ -70,6 +69,7 @@ export const CertificatesPage: React.FC = () => {
localStorage.setItem(LOCAL_STORAGE_KEY, pageVal.size.toString());
}}
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.
*/
import { 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 { Action, createAction, handleActions } from 'redux-actions';
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>(
'GET_CERTIFICATES'
);
export const setCertificatesTotalAction = createAction<CertificatesState>('SET_CERTIFICATES_TOTAL');
export interface CertificatesState {
certs: AsyncInitState<CertResult>;
total: number;
}
const initialState = {
certs: asyncInitState(),
total: 0,
};
export const certificatesReducer = handleActions<CertificatesState>(
{
...handleAsyncAction<CertificatesState>('certs', getCertificatesAction),
[String(setCertificatesTotalAction)]: (state, action: Action<CertificatesState>) => ({
...state,
total: action.payload.total,
}),
},
initialState
);
export function* fetchCertificatesEffect() {
yield takeLatest(
getCertificatesAction.get,
fetchEffectFactory(fetchCertificates, getCertificatesAction.success, getCertificatesAction.fail)
);
}
export const certificatesSelector = ({ certificates }: AppState) => certificates.certs;
export const certificatesSelector = ({ certificates }: AppState) => certificates.total;

View file

@ -16,7 +16,6 @@ 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';
import { fetchAlertsEffect } from '../alerts/alerts';
import { fetchJourneyStepsEffect } from './journey';
import { fetchNetworkEventsEffect } from './network_events';
@ -39,7 +38,6 @@ export function* rootEffect() {
yield fork(fetchMLJobEffect);
yield fork(fetchMonitorDurationEffect);
yield fork(fetchIndexStatusEffect);
yield fork(fetchCertificatesEffect);
yield fork(fetchAlertsEffect);
yield fork(fetchJourneyStepsEffect);
yield fork(fetchNetworkEventsEffect);

View file

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

View file

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

View file

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

View file

@ -58,9 +58,9 @@ export function createUptimeESClient({
return {
baseESClient: esClient,
async search<TParams extends estypes.SearchRequest>(
async search<DocumentSource extends unknown, TParams extends estypes.SearchRequest>(
params: TParams
): Promise<{ body: ESSearchResponse<unknown, TParams> }> {
): Promise<{ body: ESSearchResponse<DocumentSource, TParams> }> {
let res: any;
let esError: any;
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(
@ -155,7 +155,3 @@ export function debugESCall({
}
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({
uptimeEsClient,
index: 1,
pageIndex: 1,
from: 'now-2d',
to: 'now+1h',
search: 'my_common_name',

View file

@ -5,170 +5,37 @@
* 2.0.
*/
import { PromiseType } from 'utility-types';
import { UMElasticsearchQueryFn } from '../adapters';
import { CertResult, GetCertsParams, Ping } from '../../../common/runtime_types';
import {
getCertsRequestBody,
processCertsResult,
} from '../../../common/requests/get_certs_request_body';
import { UptimeESClient } from '../lib';
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 (
requestParams
) => {
const result = await getCertsResults(requestParams);
export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = async ({
uptimeEsClient,
index,
from,
to,
size,
search,
notValidBefore,
notValidAfter,
sortBy,
direction,
}) => {
const sort = SortFields[sortBy as keyof typeof SortFields];
return processCertsResult(result);
};
const searchBody = {
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',
},
},
},
};
export type CertificatesResults = PromiseType<ReturnType<typeof getCertsResults>>;
if (notValidBefore || notValidAfter) {
const validityFilters: any = {
bool: {
should: [],
},
};
if (notValidBefore) {
validityFilters.bool.should.push({
range: {
'tls.certificate_not_valid_before': {
lte: notValidBefore,
},
},
});
}
if (notValidAfter) {
validityFilters.bool.should.push({
range: {
'tls.certificate_not_valid_after': {
lte: notValidAfter,
},
},
});
}
const getCertsResults = async (
requestParams: GetCertsParams & { uptimeEsClient: UptimeESClient }
) => {
const { uptimeEsClient } = requestParams;
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,
});
const certs = (result?.hits?.hits ?? []).map((hit) => {
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 };
return result;
};

View file

@ -10,7 +10,7 @@ import { GetPingHistogramParams, HistogramResult } from '../../../common/runtime
import { QUERY } from '../../../common/constants';
import { getHistogramInterval } from '../helper/get_histogram_interval';
import { UMElasticsearchQueryFn } from '../adapters/framework';
import { createEsQuery } from '../lib';
import { createEsQuery } from '../../../common/utils/es_search';
export const getPingHistogram: UMElasticsearchQueryFn<
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.
*/
import { createGetCertsRoute } from './certs/certs';
import { createGetOverviewFilters } from './overview_filters';
import {
createGetPingHistogramRoute,
@ -35,7 +34,6 @@ export { createRouteWithAuth } from './create_route_with_auth';
export { uptimeRouteWrapper } from './uptime_route_wrapper';
export const restApiRoutes: UMRestApiRouteFactory[] = [
createGetCertsRoute,
createGetOverviewFilters,
createGetPingsRoute,
createGetIndexPatternRoute,

View file

@ -9,9 +9,12 @@ import expect from '@kbn/expect';
import moment from 'moment';
import { isRight } from 'fp-ts/lib/Either';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { API_URLS } from '../../../../../plugins/uptime/common/constants';
import { CertType } from '../../../../../plugins/uptime/common/runtime_types';
import { makeChecksWithStatus } from './helper/make_checks';
import {
processCertsResult,
getCertsRequestBody,
} from '../../../../../plugins/uptime/common/requests/get_certs_request_body';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
@ -21,8 +24,18 @@ export default function ({ getService }: FtrProviderContext) {
describe('certs api', () => {
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":[],"total":0}');
const apiResponse = await supertest
.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 () => {
const apiResponse = await supertest.get(API_URLS.CERTS);
const { body } = apiResponse;
const { body } = await supertest
.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);
expect(Array.isArray(body.certs)).to.be(true);
expect(body.certs).to.have.length(1);
const result = processCertsResult(body.rawResponse);
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);
const cert = body.certs[0];
const cert = result.certs[0];
expect(Array.isArray(cert.monitors)).to.be(true);
expect(cert.monitors[0]).to.eql({
name: undefined,
id: monitorId,
url: 'http://localhost:5678/pattern?r=200x5,500x1',
});