mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution][Threat Hunting] Monitor all http request using APM (#138152)
* integrate APM transactions to useSearchStrategy and useQuery hooks * fix batched request transaction using blocking span * disable transactions managed flag * apm mock * add and adapt tests * add invalid response warning to useSearchStrategy * make useQueryAlerts query name required and prop name changed * fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
62092e2e05
commit
aae3ac0da7
34 changed files with 474 additions and 149 deletions
|
@ -10,6 +10,7 @@ import type { Ecs } from '@kbn/cases-plugin/common';
|
|||
import { useSourcererDataView } from '../../common/containers/sourcerer';
|
||||
import { SourcererScopeName } from '../../common/store/sourcerer/model';
|
||||
import { useQueryAlerts } from '../../detections/containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../detections/containers/detection_engine/alerts/constants';
|
||||
import type { SignalHit } from '../../common/utils/alerts';
|
||||
import { buildAlertsQuery, formatAlertToEcsSignal } from '../../common/utils/alerts';
|
||||
|
||||
|
@ -20,6 +21,7 @@ export const useFetchAlertData = (alertIds: string[]): [boolean, Record<string,
|
|||
const { loading: isLoadingAlerts, data: alertsData } = useQueryAlerts<SignalHit, unknown>({
|
||||
query: alertsQuery,
|
||||
indexName: selectedPatterns[0],
|
||||
queryName: ALERTS_QUERY_NAMES.CASES,
|
||||
});
|
||||
|
||||
const alerts = useMemo(
|
||||
|
|
|
@ -18,6 +18,7 @@ import { AlertsTreemap, DEFAULT_MIN_CHART_HEIGHT } from '../alerts_treemap';
|
|||
import { KpiPanel } from '../../../detections/components/alerts_kpis/common/components';
|
||||
import { useInspectButton } from '../../../detections/components/alerts_kpis/common/hooks';
|
||||
import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants';
|
||||
import { FieldSelection } from '../field_selection';
|
||||
import { HeaderSection } from '../header_section';
|
||||
import { InspectButtonContainer } from '../inspect';
|
||||
|
@ -117,6 +118,7 @@ const AlertsTreemapPanelComponent: React.FC<Props> = ({
|
|||
}),
|
||||
skip: !isPanelExpanded,
|
||||
indexName: signalIndexName,
|
||||
queryName: ALERTS_QUERY_NAMES.TREE_MAP,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants';
|
|||
import { useGlobalTime } from '../use_global_time';
|
||||
import type { GenericBuckets } from '../../../../common/search_strategy';
|
||||
import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants';
|
||||
import { TimelineId } from '../../../../common/types';
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { inputsSelectors } from '../../store';
|
||||
|
@ -52,6 +53,7 @@ export const useAlertPrevalence = ({
|
|||
const { loading, data, setQuery } = useQueryAlerts<{ _id: string }, AlertPrevalenceAggregation>({
|
||||
query: initialQuery,
|
||||
indexName: signalIndexName,
|
||||
queryName: ALERTS_QUERY_NAMES.PREVALENCE,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants';
|
||||
import { useAlertsByIds } from './use_alerts_by_ids';
|
||||
|
||||
jest.mock('../../../detections/containers/detection_engine/alerts/use_query', () => ({
|
||||
|
@ -71,6 +72,7 @@ describe('useAlertsByIds', () => {
|
|||
renderHook(() => useAlertsByIds({ alertIds }));
|
||||
|
||||
expect(mockUseQueryAlerts).toHaveBeenCalledWith({
|
||||
queryName: ALERTS_QUERY_NAMES.BY_ID,
|
||||
query: expect.objectContaining({
|
||||
fields: ['*'],
|
||||
_source: false,
|
||||
|
@ -93,6 +95,7 @@ describe('useAlertsByIds', () => {
|
|||
renderHook(() => useAlertsByIds({ alertIds, fields: testFields }));
|
||||
|
||||
expect(mockUseQueryAlerts).toHaveBeenCalledWith({
|
||||
queryName: ALERTS_QUERY_NAMES.BY_ID,
|
||||
query: expect.objectContaining({ fields: testFields }),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants';
|
||||
|
||||
interface UseAlertByIdsOptions {
|
||||
alertIds: string[];
|
||||
|
@ -37,6 +38,7 @@ export const useAlertsByIds = ({
|
|||
|
||||
const { loading, data, setQuery } = useQueryAlerts<Hit, unknown>({
|
||||
query: initialQuery,
|
||||
queryName: ALERTS_QUERY_NAMES.BY_ID,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -5,21 +5,66 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useSearchStrategy } from '.';
|
||||
import { useSearch, useSearchStrategy } from '.';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useObservable } from '@kbn/securitysolution-hook-utils';
|
||||
import type { FactoryQueryTypes } from '../../../../common/search_strategy';
|
||||
import type { FactoryQueryTypes, StrategyRequestType } from '../../../../common/search_strategy';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
jest.mock('@kbn/securitysolution-hook-utils');
|
||||
const mockAddToastError = jest.fn();
|
||||
|
||||
const mockAddToastWarning = jest.fn();
|
||||
jest.mock('../../hooks/use_app_toasts', () => ({
|
||||
useAppToasts: jest.fn(() => ({
|
||||
addError: mockAddToastError,
|
||||
addWarning: mockAddToastWarning,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/securitysolution-hook-utils');
|
||||
// default to completed response
|
||||
const mockResponse = jest.fn(
|
||||
() =>
|
||||
({
|
||||
rawResponse: {},
|
||||
isPartial: false,
|
||||
isRunning: false,
|
||||
} as unknown)
|
||||
);
|
||||
const mockSearch = jest.fn(
|
||||
() =>
|
||||
new Observable((subscription) => {
|
||||
subscription.next(mockResponse());
|
||||
})
|
||||
);
|
||||
jest.mock('../../lib/kibana', () => {
|
||||
const original = jest.requireActual('../../lib/kibana');
|
||||
return {
|
||||
...original,
|
||||
useKibana: () => ({
|
||||
...original.useKibana(),
|
||||
services: {
|
||||
...original.useKibana().services,
|
||||
data: {
|
||||
search: {
|
||||
search: mockSearch,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const mockEndTracking = jest.fn();
|
||||
const mockStartTracking = jest.fn(() => ({
|
||||
endTracking: mockEndTracking,
|
||||
}));
|
||||
jest.mock('../../lib/apm/use_track_http_request', () => ({
|
||||
useTrackHttpRequest: () => ({ startTracking: mockStartTracking }),
|
||||
}));
|
||||
|
||||
const mockAbortController = new AbortController();
|
||||
mockAbortController.abort = jest.fn();
|
||||
|
||||
const useObservableHookResult = {
|
||||
start: jest.fn(),
|
||||
|
@ -28,13 +73,24 @@ const useObservableHookResult = {
|
|||
loading: false,
|
||||
};
|
||||
|
||||
const factoryQueryType = 'testFactoryQueryType' as FactoryQueryTypes;
|
||||
const userSearchStrategyProps = {
|
||||
factoryQueryType: 'testFactoryQueryType' as FactoryQueryTypes,
|
||||
factoryQueryType,
|
||||
initialResult: {},
|
||||
errorMessage: 'testErrorMessage',
|
||||
};
|
||||
|
||||
const request = {
|
||||
fake: 'request',
|
||||
search: 'parameters',
|
||||
} as unknown as StrategyRequestType<FactoryQueryTypes>;
|
||||
|
||||
describe('useSearchStrategy', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(window, 'AbortController').mockRestore();
|
||||
});
|
||||
|
||||
it("returns the provided initial result while the query hasn't returned data", () => {
|
||||
const initialResult = {};
|
||||
(useObservable as jest.Mock).mockReturnValue(useObservableHookResult);
|
||||
|
@ -46,22 +102,17 @@ describe('useSearchStrategy', () => {
|
|||
expect(result.current.result).toBe(initialResult);
|
||||
});
|
||||
|
||||
it('calls start with the given factoryQueryType', () => {
|
||||
const factoryQueryType = 'fakeQueryType' as FactoryQueryTypes;
|
||||
it('calls start with the given request', () => {
|
||||
const start = jest.fn();
|
||||
|
||||
(useObservable as jest.Mock).mockReturnValue({ ...useObservableHookResult, start });
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSearchStrategy<FactoryQueryTypes>({
|
||||
...userSearchStrategyProps,
|
||||
factoryQueryType,
|
||||
})
|
||||
useSearchStrategy<FactoryQueryTypes>(userSearchStrategyProps)
|
||||
);
|
||||
|
||||
result.current.search({});
|
||||
|
||||
expect(start).toBeCalledWith(expect.objectContaining({ factoryQueryType }));
|
||||
result.current.search(request);
|
||||
expect(start).toBeCalledWith(expect.objectContaining({ request }));
|
||||
});
|
||||
|
||||
it('returns inspect', () => {
|
||||
|
@ -104,7 +155,6 @@ describe('useSearchStrategy', () => {
|
|||
|
||||
it('start should be called when search is called ', () => {
|
||||
const start = jest.fn();
|
||||
const searchParams = {};
|
||||
|
||||
(useObservable as jest.Mock).mockReturnValue({ ...useObservableHookResult, start });
|
||||
|
||||
|
@ -112,14 +162,13 @@ describe('useSearchStrategy', () => {
|
|||
useSearchStrategy<FactoryQueryTypes>(userSearchStrategyProps)
|
||||
);
|
||||
|
||||
result.current.search(searchParams);
|
||||
result.current.search(request);
|
||||
|
||||
expect(start).toBeCalled();
|
||||
});
|
||||
|
||||
it('refetch should execute the previous search again with the same params', async () => {
|
||||
const start = jest.fn();
|
||||
const searchParams = {};
|
||||
|
||||
(useObservable as jest.Mock).mockReturnValue({ ...useObservableHookResult, start });
|
||||
|
||||
|
@ -127,7 +176,7 @@ describe('useSearchStrategy', () => {
|
|||
useSearchStrategy<FactoryQueryTypes>(userSearchStrategyProps)
|
||||
);
|
||||
|
||||
result.current.search(searchParams);
|
||||
result.current.search(request);
|
||||
|
||||
rerender();
|
||||
|
||||
|
@ -138,11 +187,7 @@ describe('useSearchStrategy', () => {
|
|||
});
|
||||
|
||||
it('aborts previous search when a subsequent search is triggered', async () => {
|
||||
const searchParams = {};
|
||||
const abortFunction = jest.fn();
|
||||
jest
|
||||
.spyOn(window, 'AbortController')
|
||||
.mockReturnValue({ abort: abortFunction, signal: {} as AbortSignal });
|
||||
jest.spyOn(window, 'AbortController').mockReturnValue(mockAbortController);
|
||||
|
||||
(useObservable as jest.Mock).mockReturnValue(useObservableHookResult);
|
||||
|
||||
|
@ -150,18 +195,14 @@ describe('useSearchStrategy', () => {
|
|||
useSearchStrategy<FactoryQueryTypes>(userSearchStrategyProps)
|
||||
);
|
||||
|
||||
result.current.search(searchParams);
|
||||
result.current.search(searchParams);
|
||||
result.current.search(request);
|
||||
result.current.search(request);
|
||||
|
||||
expect(abortFunction).toBeCalledTimes(2);
|
||||
expect(mockAbortController.abort).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('aborts search when component unmounts', async () => {
|
||||
const searchParams = {};
|
||||
const abortFunction = jest.fn();
|
||||
jest
|
||||
.spyOn(window, 'AbortController')
|
||||
.mockReturnValue({ abort: abortFunction, signal: {} as AbortSignal });
|
||||
jest.spyOn(window, 'AbortController').mockReturnValue(mockAbortController);
|
||||
|
||||
(useObservable as jest.Mock).mockReturnValue(useObservableHookResult);
|
||||
|
||||
|
@ -169,44 +210,95 @@ describe('useSearchStrategy', () => {
|
|||
useSearchStrategy<FactoryQueryTypes>(userSearchStrategyProps)
|
||||
);
|
||||
|
||||
result.current.search(searchParams);
|
||||
result.current.search(request);
|
||||
unmount();
|
||||
|
||||
expect(abortFunction).toBeCalledTimes(2);
|
||||
expect(mockAbortController.abort).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('calls start with the AbortController signal', () => {
|
||||
const factoryQueryType = 'fakeQueryType' as FactoryQueryTypes;
|
||||
jest.spyOn(window, 'AbortController').mockReturnValue(mockAbortController);
|
||||
const start = jest.fn();
|
||||
const signal = new AbortController().signal;
|
||||
jest.spyOn(window, 'AbortController').mockReturnValue({ abort: jest.fn(), signal });
|
||||
|
||||
(useObservable as jest.Mock).mockReturnValue({ ...useObservableHookResult, start });
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSearchStrategy<FactoryQueryTypes>({
|
||||
...userSearchStrategyProps,
|
||||
factoryQueryType,
|
||||
})
|
||||
useSearchStrategy<FactoryQueryTypes>(userSearchStrategyProps)
|
||||
);
|
||||
|
||||
result.current.search({});
|
||||
result.current.search(request);
|
||||
|
||||
expect(start).toBeCalledWith(expect.objectContaining({ signal }));
|
||||
expect(start).toBeCalledWith(
|
||||
expect.objectContaining({ abortSignal: mockAbortController.signal })
|
||||
);
|
||||
});
|
||||
|
||||
it('abort = true will cancel any running request', () => {
|
||||
const abortSpy = jest.fn();
|
||||
const signal = new AbortController().signal;
|
||||
jest.spyOn(window, 'AbortController').mockReturnValue({ abort: abortSpy, signal });
|
||||
const factoryQueryType = 'fakeQueryType' as FactoryQueryTypes;
|
||||
const localProps = {
|
||||
...userSearchStrategyProps,
|
||||
abort: false,
|
||||
factoryQueryType,
|
||||
};
|
||||
jest.spyOn(window, 'AbortController').mockReturnValue(mockAbortController);
|
||||
const localProps = { ...userSearchStrategyProps, abort: false };
|
||||
|
||||
const { rerender } = renderHook(() => useSearchStrategy<FactoryQueryTypes>(localProps));
|
||||
localProps.abort = true;
|
||||
act(() => rerender());
|
||||
expect(abortSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockAbortController.abort).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('search function', () => {
|
||||
it('should track successful search result', () => {
|
||||
const { result } = renderHook(() => useSearch<FactoryQueryTypes>(factoryQueryType));
|
||||
result.current({ request, abortSignal: new AbortController().signal });
|
||||
|
||||
expect(mockStartTracking).toBeCalledTimes(1);
|
||||
expect(mockEndTracking).toBeCalledTimes(1);
|
||||
expect(mockEndTracking).toBeCalledWith('success');
|
||||
});
|
||||
|
||||
it('should track invalid search result', () => {
|
||||
mockResponse.mockReturnValueOnce({}); // mock invalid empty response
|
||||
|
||||
const { result } = renderHook(() => useSearch<FactoryQueryTypes>(factoryQueryType));
|
||||
result.current({ request, abortSignal: new AbortController().signal });
|
||||
|
||||
expect(mockStartTracking).toBeCalledTimes(1);
|
||||
expect(mockEndTracking).toBeCalledWith('invalid');
|
||||
});
|
||||
|
||||
it('should track error search result', () => {
|
||||
mockResponse.mockImplementationOnce(() => {
|
||||
throw Error('fake server error');
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSearch<FactoryQueryTypes>(factoryQueryType));
|
||||
result.current({ request, abortSignal: new AbortController().signal });
|
||||
|
||||
expect(mockStartTracking).toBeCalledTimes(1);
|
||||
expect(mockEndTracking).toBeCalledTimes(1);
|
||||
expect(mockEndTracking).toBeCalledWith('error');
|
||||
});
|
||||
|
||||
it('should track aborted search result', () => {
|
||||
const abortController = new AbortController();
|
||||
mockResponse.mockImplementationOnce(() => {
|
||||
abortController.abort();
|
||||
throw Error('fake aborted');
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSearch<FactoryQueryTypes>(factoryQueryType));
|
||||
result.current({ request, abortSignal: abortController.signal });
|
||||
|
||||
expect(mockStartTracking).toBeCalledTimes(1);
|
||||
expect(mockEndTracking).toBeCalledTimes(1);
|
||||
expect(mockEndTracking).toBeCalledWith('aborted');
|
||||
});
|
||||
|
||||
it('should show toast warning when the API returns partial invalid response', () => {
|
||||
mockResponse.mockReturnValueOnce({}); // mock invalid empty response
|
||||
|
||||
const { result } = renderHook(() => useSearch<FactoryQueryTypes>(factoryQueryType));
|
||||
result.current({ request, abortSignal: new AbortController().signal });
|
||||
|
||||
expect(mockAddToastWarning).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,19 +8,13 @@ import { filter } from 'rxjs/operators';
|
|||
import { noop, omit } from 'lodash/fp';
|
||||
import { useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import type { Observable } from 'rxjs';
|
||||
|
||||
import type { OptionalSignalArgs } from '@kbn/securitysolution-hook-utils';
|
||||
import { useObservable } from '@kbn/securitysolution-hook-utils';
|
||||
|
||||
import type { IKibanaSearchResponse } from '@kbn/data-plugin/common';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/public';
|
||||
import { AbortError } from '@kbn/kibana-utils-plugin/common';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import type {
|
||||
FactoryQueryTypes,
|
||||
RequestBasicOptions,
|
||||
StrategyRequestType,
|
||||
StrategyResponseType,
|
||||
} from '../../../../common/search_strategy/security_solution';
|
||||
|
@ -28,52 +22,73 @@ import { getInspectResponse } from '../../../helpers';
|
|||
import type { inputsModel } from '../../store';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { useAppToasts } from '../../hooks/use_app_toasts';
|
||||
import { useTrackHttpRequest } from '../../lib/apm/use_track_http_request';
|
||||
import { APP_UI_ID } from '../../../../common/constants';
|
||||
|
||||
type UseSearchStrategyRequestArgs = RequestBasicOptions & {
|
||||
data: DataPublicPluginStart;
|
||||
signal: AbortSignal;
|
||||
factoryQueryType: FactoryQueryTypes;
|
||||
};
|
||||
interface UseSearchFunctionParams<QueryType extends FactoryQueryTypes> {
|
||||
request: StrategyRequestType<QueryType>;
|
||||
abortSignal: AbortSignal;
|
||||
}
|
||||
|
||||
const search = <ResponseType extends IKibanaSearchResponse>({
|
||||
data,
|
||||
signal,
|
||||
factoryQueryType,
|
||||
defaultIndex,
|
||||
filterQuery,
|
||||
timerange,
|
||||
...requestProps
|
||||
}: UseSearchStrategyRequestArgs): Observable<ResponseType> => {
|
||||
return data.search.search<RequestBasicOptions, ResponseType>(
|
||||
{
|
||||
...requestProps,
|
||||
factoryQueryType,
|
||||
defaultIndex,
|
||||
timerange,
|
||||
filterQuery,
|
||||
},
|
||||
{
|
||||
strategy: 'securitySolutionSearchStrategy',
|
||||
abortSignal: signal,
|
||||
}
|
||||
);
|
||||
};
|
||||
type UseSearchFunction<QueryType extends FactoryQueryTypes> = (
|
||||
params: UseSearchFunctionParams<QueryType>
|
||||
) => Observable<StrategyResponseType<QueryType>>;
|
||||
|
||||
const searchComplete = <ResponseType extends IKibanaSearchResponse>(
|
||||
props: UseSearchStrategyRequestArgs
|
||||
): Observable<ResponseType> => {
|
||||
return search<ResponseType>(props).pipe(
|
||||
filter((response) => {
|
||||
return isErrorResponse(response) || isCompleteResponse(response);
|
||||
})
|
||||
);
|
||||
};
|
||||
type SearchFunction<QueryType extends FactoryQueryTypes> = (
|
||||
params: StrategyRequestType<QueryType>
|
||||
) => void;
|
||||
|
||||
const EMPTY_INSPECT = {
|
||||
dsl: [],
|
||||
response: [],
|
||||
};
|
||||
|
||||
export const useSearch = <QueryType extends FactoryQueryTypes>(
|
||||
factoryQueryType: QueryType
|
||||
): UseSearchFunction<QueryType> => {
|
||||
const { data } = useKibana().services;
|
||||
const { addWarning } = useAppToasts();
|
||||
const { startTracking } = useTrackHttpRequest();
|
||||
|
||||
const search = useCallback<UseSearchFunction<QueryType>>(
|
||||
({ abortSignal, request }) => {
|
||||
const { endTracking } = startTracking({
|
||||
name: `${APP_UI_ID} searchStrategy ${factoryQueryType}`,
|
||||
spanName: 'batched search',
|
||||
});
|
||||
|
||||
const observable = data.search
|
||||
.search<StrategyRequestType<QueryType>, StrategyResponseType<QueryType>>(
|
||||
{ ...request, factoryQueryType },
|
||||
{
|
||||
strategy: 'securitySolutionSearchStrategy',
|
||||
abortSignal,
|
||||
}
|
||||
)
|
||||
.pipe(filter((response) => isErrorResponse(response) || isCompleteResponse(response)));
|
||||
|
||||
observable.subscribe({
|
||||
next: (response) => {
|
||||
if (isErrorResponse(response)) {
|
||||
addWarning(i18n.INVALID_RESPONSE_WARNING_SEARCH_STRATEGY(factoryQueryType));
|
||||
endTracking('invalid');
|
||||
} else {
|
||||
endTracking('success');
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
endTracking(abortSignal.aborted ? 'aborted' : 'error');
|
||||
},
|
||||
});
|
||||
|
||||
return observable;
|
||||
},
|
||||
[addWarning, data.search, factoryQueryType, startTracking]
|
||||
);
|
||||
|
||||
return search;
|
||||
};
|
||||
|
||||
export const useSearchStrategy = <QueryType extends FactoryQueryTypes>({
|
||||
factoryQueryType,
|
||||
initialResult,
|
||||
|
@ -86,7 +101,7 @@ export const useSearchStrategy = <QueryType extends FactoryQueryTypes>({
|
|||
*/
|
||||
initialResult: Omit<StrategyResponseType<QueryType>, 'rawResponse'>;
|
||||
/**
|
||||
* Message displayed to the user on a Toast when an erro happens.
|
||||
* Message displayed to the user on a Toast when an error happens.
|
||||
*/
|
||||
errorMessage?: string;
|
||||
/**
|
||||
|
@ -95,15 +110,15 @@ export const useSearchStrategy = <QueryType extends FactoryQueryTypes>({
|
|||
abort?: boolean;
|
||||
}) => {
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
||||
const refetch = useRef<inputsModel.Refetch>(noop);
|
||||
const { data } = useKibana().services;
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
const search = useSearch(factoryQueryType);
|
||||
|
||||
const { start, error, result, loading } = useObservable<
|
||||
[UseSearchStrategyRequestArgs],
|
||||
[UseSearchFunctionParams<QueryType>],
|
||||
StrategyResponseType<QueryType>
|
||||
>(searchComplete);
|
||||
>(search);
|
||||
|
||||
useEffect(() => {
|
||||
if (error != null && !(error instanceof AbortError)) {
|
||||
|
@ -113,24 +128,22 @@ export const useSearchStrategy = <QueryType extends FactoryQueryTypes>({
|
|||
}
|
||||
}, [addError, error, errorMessage, factoryQueryType]);
|
||||
|
||||
const searchCb = useCallback(
|
||||
(props: OptionalSignalArgs<StrategyRequestType<QueryType>>) => {
|
||||
const asyncSearch = () => {
|
||||
const searchCb = useCallback<SearchFunction<QueryType>>(
|
||||
(request) => {
|
||||
const startSearch = () => {
|
||||
abortCtrl.current = new AbortController();
|
||||
start({
|
||||
...props,
|
||||
data,
|
||||
factoryQueryType,
|
||||
signal: abortCtrl.current.signal,
|
||||
} as never); // This typescast is required because every StrategyRequestType instance has different fields.
|
||||
request,
|
||||
abortSignal: abortCtrl.current.signal,
|
||||
});
|
||||
};
|
||||
|
||||
abortCtrl.current.abort();
|
||||
asyncSearch();
|
||||
startSearch();
|
||||
|
||||
refetch.current = asyncSearch;
|
||||
refetch.current = startSearch;
|
||||
},
|
||||
[data, start, factoryQueryType]
|
||||
[start]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -145,19 +158,19 @@ export const useSearchStrategy = <QueryType extends FactoryQueryTypes>({
|
|||
}
|
||||
}, [abort]);
|
||||
|
||||
const [formatedResult, inspect] = useMemo(
|
||||
() => [
|
||||
result
|
||||
? omit<StrategyResponseType<QueryType>, 'rawResponse'>('rawResponse', result)
|
||||
: initialResult,
|
||||
result ? getInspectResponse(result, EMPTY_INSPECT) : EMPTY_INSPECT,
|
||||
],
|
||||
[result, initialResult]
|
||||
);
|
||||
const [formattedResult, inspect] = useMemo(() => {
|
||||
if (isErrorResponse(result)) {
|
||||
return [initialResult, EMPTY_INSPECT];
|
||||
}
|
||||
return [
|
||||
omit<StrategyResponseType<QueryType>, 'rawResponse'>('rawResponse', result),
|
||||
getInspectResponse(result, EMPTY_INSPECT),
|
||||
];
|
||||
}, [result, initialResult]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
result: formatedResult,
|
||||
result: formattedResult,
|
||||
error,
|
||||
search: searchCb,
|
||||
refetch: refetch.current,
|
||||
|
|
|
@ -13,3 +13,9 @@ export const DEFAULT_ERROR_SEARCH_STRATEGY = (factoryQueryType: FactoryQueryType
|
|||
values: { factoryQueryType },
|
||||
defaultMessage: `Failed to run search: {factoryQueryType}`,
|
||||
});
|
||||
|
||||
export const INVALID_RESPONSE_WARNING_SEARCH_STRATEGY = (factoryQueryType: FactoryQueryTypes) =>
|
||||
i18n.translate('xpack.securitySolution.searchStrategy.warning', {
|
||||
values: { factoryQueryType },
|
||||
defaultMessage: `An error has occurred running search: {factoryQueryType}`,
|
||||
});
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
const mockStartTracking = jest.fn(() => ({ endTracking: jest.fn() }));
|
||||
export const useTrackHttpRequest = jest.fn(() => ({
|
||||
startTracking: mockStartTracking,
|
||||
}));
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const mockApm = () => ({
|
||||
startTransaction: jest.fn(() => ({
|
||||
addLabels: jest.fn(),
|
||||
end: jest.fn(),
|
||||
startSpan: jest.fn(() => ({
|
||||
end: jest.fn(),
|
||||
})),
|
||||
})),
|
||||
});
|
|
@ -6,13 +6,15 @@
|
|||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import type { TransactionOptions } from '@elastic/apm-rum';
|
||||
import { useKibana } from '../kibana';
|
||||
|
||||
const transactionOptions = { managed: true };
|
||||
const DEFAULT_TRANSACTION_OPTIONS: TransactionOptions = { managed: true };
|
||||
|
||||
interface StartTransactionOptions {
|
||||
name: string;
|
||||
type?: string;
|
||||
options?: TransactionOptions;
|
||||
}
|
||||
|
||||
export const useStartTransaction = () => {
|
||||
|
@ -21,8 +23,8 @@ export const useStartTransaction = () => {
|
|||
} = useKibana();
|
||||
|
||||
const startTransaction = useCallback(
|
||||
({ name, type = 'user-interaction' }: StartTransactionOptions) => {
|
||||
return apm.startTransaction(name, type, transactionOptions);
|
||||
({ name, type = 'user-interaction', options }: StartTransactionOptions) => {
|
||||
return apm.startTransaction(name, type, options ?? DEFAULT_TRANSACTION_OPTIONS);
|
||||
},
|
||||
[apm]
|
||||
);
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useCallback } from 'react';
|
||||
import { useStartTransaction } from './use_start_transaction';
|
||||
|
||||
interface UseTrackHttpRequestOptions {
|
||||
name: string;
|
||||
spanName?: string;
|
||||
}
|
||||
|
||||
export type RequestResult = 'success' | 'error' | 'aborted' | 'invalid';
|
||||
|
||||
export const useTrackHttpRequest = () => {
|
||||
const { startTransaction } = useStartTransaction();
|
||||
|
||||
const startTracking = useCallback(
|
||||
({ name, spanName = 'fetch' }: UseTrackHttpRequestOptions) => {
|
||||
// Create the transaction, the managed flag is turned off to prevent it from being polluted by non-related automatic spans.
|
||||
// The managed flag can be turned on to investigate high latency requests in APM.
|
||||
// However, note that by enabling the managed flag, the transaction trace may be distorted by other requests information.
|
||||
const transaction = startTransaction({
|
||||
name,
|
||||
type: 'http-request',
|
||||
options: { managed: false },
|
||||
});
|
||||
// Create a blocking span to control the transaction time and prevent it from closing automatically with partial batch responses.
|
||||
// The blocking span needs to be ended manually when the request finishes.
|
||||
const span = transaction?.startSpan(spanName, 'http-request', {
|
||||
blocking: true,
|
||||
});
|
||||
return {
|
||||
endTracking: (result: RequestResult): void => {
|
||||
transaction?.addLabels({ result });
|
||||
span?.end();
|
||||
},
|
||||
};
|
||||
},
|
||||
[startTransaction]
|
||||
);
|
||||
|
||||
return { startTracking };
|
||||
};
|
|
@ -16,6 +16,7 @@ import {
|
|||
createStartServicesMock,
|
||||
createWithKibanaMock,
|
||||
} from '../kibana_react.mock';
|
||||
import { mockApm } from '../../apm/service.mock';
|
||||
import { APP_UI_ID } from '../../../../../common/constants';
|
||||
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
|
||||
|
||||
|
@ -24,6 +25,7 @@ export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() =>
|
|||
export const useKibana = jest.fn().mockReturnValue({
|
||||
services: {
|
||||
...mockStartServicesMock,
|
||||
apm: mockApm(),
|
||||
uiSettings: {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
/* eslint-disable react/display-name */
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { RecursivePartial } from '@elastic/eui/src/components/common';
|
||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
import { coreMock, themeServiceMock } from '@kbn/core/public/mocks';
|
||||
|
@ -43,6 +42,7 @@ import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
|
|||
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
|
||||
import { noCasesPermissions } from '../../../cases_test_utils';
|
||||
import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';
|
||||
import { mockApm } from '../apm/service.mock';
|
||||
|
||||
const mockUiSettings: Record<string, unknown> = {
|
||||
[DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' },
|
||||
|
@ -94,6 +94,7 @@ export const createStartServicesMock = (
|
|||
): StartServices => {
|
||||
core.uiSettings.get.mockImplementation(createUseUiSettingMock());
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const apm = mockApm();
|
||||
const data = dataPluginMock.createStartContract();
|
||||
const security = securityMock.createSetup();
|
||||
const urlService = new MockUrlService();
|
||||
|
@ -106,6 +107,7 @@ export const createStartServicesMock = (
|
|||
|
||||
return {
|
||||
...core,
|
||||
apm,
|
||||
cases,
|
||||
unifiedSearch,
|
||||
data: {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
|||
import { HeaderSection } from '../../../../common/components/header_section';
|
||||
|
||||
import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../containers/detection_engine/alerts/constants';
|
||||
import { InspectButtonContainer } from '../../../../common/components/inspect';
|
||||
|
||||
import { getAlertsCountQuery } from './helpers';
|
||||
|
@ -125,6 +126,7 @@ export const AlertsCountPanel = memo<AlertsCountPanelProps>(
|
|||
}),
|
||||
indexName: signalIndexName,
|
||||
skip: querySkip,
|
||||
queryName: ALERTS_QUERY_NAMES.COUNT,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -25,6 +25,7 @@ import type { LegendItem } from '../../../../common/components/charts/draggable_
|
|||
import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers';
|
||||
import { HeaderSection } from '../../../../common/components/header_section';
|
||||
import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../containers/detection_engine/alerts/constants';
|
||||
import { getDetectionEngineUrl, useFormatUrl } from '../../../../common/components/link_to';
|
||||
import { defaultLegendColors } from '../../../../common/components/matrix_histogram/utils';
|
||||
import { InspectButtonContainer } from '../../../../common/components/inspect';
|
||||
|
@ -191,6 +192,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
|||
),
|
||||
indexName: signalIndexName,
|
||||
skip: querySkip,
|
||||
queryName: ALERTS_QUERY_NAMES.HISTOGRAM,
|
||||
});
|
||||
|
||||
const kibana = useKibana();
|
||||
|
|
|
@ -29,6 +29,7 @@ import { inputsSelectors } from '../../../../common/store';
|
|||
import { TimelineId } from '../../../../../common/types';
|
||||
import type { AlertData, EcsHit } from '../../../../common/components/exceptions/types';
|
||||
import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../containers/detection_engine/alerts/constants';
|
||||
import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index';
|
||||
import { EventFiltersFlyout } from '../../../../management/pages/event_filters/view/components/event_filters_flyout';
|
||||
import { useAlertsActions } from './use_alerts_actions';
|
||||
|
@ -315,6 +316,7 @@ export const AddExceptionFlyoutWrapper: React.FC<AddExceptionFlyoutWrapperProps>
|
|||
const { loading: isLoadingAlertData, data } = useQueryAlerts<EcsHit, {}>({
|
||||
query: buildGetAlertByIdQuery(eventId),
|
||||
indexName: signalIndexName,
|
||||
queryName: ALERTS_QUERY_NAMES.ADD_EXCEPTION_FLYOUT,
|
||||
});
|
||||
|
||||
const enrichedAlert: AlertData | undefined = useMemo(() => {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { APP_UI_ID } from '../../../../../common/constants';
|
||||
|
||||
export const ALERTS_QUERY_NAMES = {
|
||||
ADD_EXCEPTION_FLYOUT: `${APP_UI_ID} fetchAlerts addExceptionFlyout`,
|
||||
BY_ID: `${APP_UI_ID} fetchAlerts byId`,
|
||||
BY_RULE_ID: `${APP_UI_ID} fetchAlerts byRuleId`,
|
||||
BY_SEVERITY: `${APP_UI_ID} fetchAlerts bySeverity`,
|
||||
BY_STATUS: `${APP_UI_ID} fetchAlerts byStatus`,
|
||||
CASES: `${APP_UI_ID} fetchAlerts cases`,
|
||||
COUNT: `${APP_UI_ID} fetchAlerts count`,
|
||||
HISTOGRAM: `${APP_UI_ID} fetchAlerts histogram`,
|
||||
PREVALENCE: `${APP_UI_ID} fetchAlerts prevalence`,
|
||||
TREE_MAP: `${APP_UI_ID} fetchAlerts treeMap`,
|
||||
VULNERABLE_HOSTS: `${APP_UI_ID} fetchAlerts vulnerableHosts`,
|
||||
VULNERABLE_USERS: `${APP_UI_ID} fetchAlerts vulnerableUsers`,
|
||||
} as const;
|
|
@ -8,22 +8,31 @@
|
|||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import type { ReturnQueryAlerts } from './use_query';
|
||||
import { useQueryAlerts } from './use_query';
|
||||
import { ALERTS_QUERY_NAMES } from './constants';
|
||||
import * as api from './api';
|
||||
import { mockAlertsQuery, alertsMock } from './mock';
|
||||
|
||||
jest.mock('./api');
|
||||
jest.mock('../../../../common/lib/apm/use_track_http_request');
|
||||
|
||||
const indexName = 'mock-index-name';
|
||||
const defaultProps = {
|
||||
query: mockAlertsQuery,
|
||||
indexName,
|
||||
queryName: ALERTS_QUERY_NAMES.COUNT,
|
||||
};
|
||||
|
||||
describe('useQueryAlerts', () => {
|
||||
const indexName = 'mock-index-name';
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
[object, string],
|
||||
ReturnQueryAlerts<unknown, unknown>
|
||||
>(() => useQueryAlerts<unknown, unknown>({ query: mockAlertsQuery, indexName }));
|
||||
>(() => useQueryAlerts<unknown, unknown>(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
loading: false,
|
||||
|
@ -41,7 +50,7 @@ describe('useQueryAlerts', () => {
|
|||
const { result, waitForNextUpdate } = renderHook<
|
||||
[object, string],
|
||||
ReturnQueryAlerts<unknown, unknown>
|
||||
>(() => useQueryAlerts<unknown, unknown>({ query: mockAlertsQuery, indexName }));
|
||||
>(() => useQueryAlerts<unknown, unknown>(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
|
@ -61,7 +70,7 @@ describe('useQueryAlerts', () => {
|
|||
const { result, waitForNextUpdate } = renderHook<
|
||||
[object, string],
|
||||
ReturnQueryAlerts<unknown, unknown>
|
||||
>(() => useQueryAlerts<unknown, unknown>({ query: mockAlertsQuery, indexName }));
|
||||
>(() => useQueryAlerts<unknown, unknown>(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
if (result.current.refetch) {
|
||||
|
@ -78,7 +87,7 @@ describe('useQueryAlerts', () => {
|
|||
const { rerender, waitForNextUpdate } = renderHook<
|
||||
[object, string],
|
||||
ReturnQueryAlerts<unknown, unknown>
|
||||
>((args) => useQueryAlerts({ query: args[0], indexName: args[1] }), {
|
||||
>((args) => useQueryAlerts({ ...defaultProps, query: args[0], indexName: args[1] }), {
|
||||
initialProps: [mockAlertsQuery, indexName],
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
@ -95,7 +104,7 @@ describe('useQueryAlerts', () => {
|
|||
const { result, waitForNextUpdate } = renderHook<
|
||||
[object, string],
|
||||
ReturnQueryAlerts<unknown, unknown>
|
||||
>((args) => useQueryAlerts({ query: args[0], indexName: args[1] }), {
|
||||
>((args) => useQueryAlerts({ ...defaultProps, query: args[0], indexName: args[1] }), {
|
||||
initialProps: [mockAlertsQuery, indexName],
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
@ -115,8 +124,7 @@ describe('useQueryAlerts', () => {
|
|||
});
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnQueryAlerts<unknown, unknown>>(
|
||||
() =>
|
||||
useQueryAlerts<unknown, unknown>({ query: mockAlertsQuery, indexName: 'mock-index-name' })
|
||||
() => useQueryAlerts<unknown, unknown>(defaultProps)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
@ -134,7 +142,7 @@ describe('useQueryAlerts', () => {
|
|||
test('skip', async () => {
|
||||
const abortSpy = jest.spyOn(AbortController.prototype, 'abort');
|
||||
await act(async () => {
|
||||
const localProps = { query: mockAlertsQuery, indexName, skip: false };
|
||||
const localProps = { ...defaultProps, skip: false };
|
||||
const { rerender, waitForNextUpdate } = renderHook<
|
||||
[object, string],
|
||||
ReturnQueryAlerts<unknown, unknown>
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useMemo, useEffect, useState } from 'react';
|
||||
import type { SetStateAction } from 'react';
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { fetchQueryRuleRegistryAlerts } from './api';
|
||||
import { fetchQueryAlerts } from './api';
|
||||
import type { AlertSearchResponse } from './types';
|
||||
import type { AlertSearchResponse, QueryAlerts } from './types';
|
||||
import { useTrackHttpRequest } from '../../../../common/lib/apm/use_track_http_request';
|
||||
import type { ALERTS_QUERY_NAMES } from './constants';
|
||||
|
||||
type Func = () => Promise<void>;
|
||||
|
||||
|
@ -25,13 +26,45 @@ export interface ReturnQueryAlerts<Hit, Aggs> {
|
|||
refetch: Func | null;
|
||||
}
|
||||
|
||||
interface AlertsQueryParams {
|
||||
fetchMethod?: typeof fetchQueryAlerts | typeof fetchQueryRuleRegistryAlerts;
|
||||
type AlertsQueryName = typeof ALERTS_QUERY_NAMES[keyof typeof ALERTS_QUERY_NAMES];
|
||||
|
||||
type FetchMethod = typeof fetchQueryAlerts | typeof fetchQueryRuleRegistryAlerts;
|
||||
export interface AlertsQueryParams {
|
||||
fetchMethod?: FetchMethod;
|
||||
query: object;
|
||||
indexName?: string | null;
|
||||
skip?: boolean;
|
||||
/**
|
||||
* The query name is used for performance monitoring with APM
|
||||
*/
|
||||
queryName: AlertsQueryName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapped `fetchMethod` hook that integrates
|
||||
* http-request monitoring using APM transactions.
|
||||
*/
|
||||
const useTrackedFetchMethod = (fetchMethod: FetchMethod, queryName: string): FetchMethod => {
|
||||
const { startTracking } = useTrackHttpRequest();
|
||||
|
||||
const monitoredFetchMethod = useMemo<FetchMethod>(() => {
|
||||
return async <Hit, Aggs>(params: QueryAlerts) => {
|
||||
const { endTracking } = startTracking({ name: queryName });
|
||||
let result: AlertSearchResponse<Hit, Aggs>;
|
||||
try {
|
||||
result = await fetchMethod<Hit, Aggs>(params);
|
||||
endTracking('success');
|
||||
} catch (err) {
|
||||
endTracking(params.signal.aborted ? 'aborted' : 'error');
|
||||
throw err;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}, [fetchMethod, queryName, startTracking]);
|
||||
|
||||
return monitoredFetchMethod;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for fetching Alerts from the Detection Engine API
|
||||
*
|
||||
|
@ -43,6 +76,7 @@ export const useQueryAlerts = <Hit, Aggs>({
|
|||
query: initialQuery,
|
||||
indexName,
|
||||
skip,
|
||||
queryName,
|
||||
}: AlertsQueryParams): ReturnQueryAlerts<Hit, Aggs> => {
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [alerts, setAlerts] = useState<
|
||||
|
@ -56,6 +90,8 @@ export const useQueryAlerts = <Hit, Aggs>({
|
|||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchAlerts = useTrackedFetchMethod(fetchMethod, queryName);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
@ -64,7 +100,7 @@ export const useQueryAlerts = <Hit, Aggs>({
|
|||
try {
|
||||
setLoading(true);
|
||||
|
||||
const alertResponse = await fetchMethod<Hit, Aggs>({
|
||||
const alertResponse = await fetchAlerts<Hit, Aggs>({
|
||||
query,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
@ -107,7 +143,7 @@ export const useQueryAlerts = <Hit, Aggs>({
|
|||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [query, indexName, skip, fetchMethod]);
|
||||
}, [query, indexName, skip, fetchAlerts]);
|
||||
|
||||
return { loading, ...alerts };
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
|||
jest.mock('./api');
|
||||
jest.mock('../alerts/api');
|
||||
jest.mock('../../../../common/hooks/use_app_toasts');
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
const mockNotFoundErrorForRule = () => {
|
||||
(api.fetchRuleById as jest.Mock).mockImplementation(async () => {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { expandDottedObject } from '../../../../../common/utils/expand_dotted';
|
|||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import type { AlertSearchResponse } from '../alerts/types';
|
||||
import { useQueryAlerts } from '../alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../alerts/constants';
|
||||
import { fetchRuleById } from './api';
|
||||
import { transformInput } from './transforms';
|
||||
import * as i18n from './translations';
|
||||
|
@ -98,6 +99,7 @@ export const useRuleWithFallback = (ruleId: string): UseRuleWithFallback => {
|
|||
const { loading: alertsLoading, data: alertsData } = useQueryAlerts<AlertHit, undefined>({
|
||||
query: buildLastAlertQuery(ruleId),
|
||||
skip: isExistingRule,
|
||||
queryName: ALERTS_QUERY_NAMES.BY_RULE_ID,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -70,7 +70,6 @@ jest.mock('../../../common/components/visualization_actions', () => ({
|
|||
describe('body', () => {
|
||||
const scenariosMap = {
|
||||
[HostsTableType.authentications]: 'AuthenticationsQueryTabBody',
|
||||
[HostsTableType.hosts]: 'HostsQueryTabBody',
|
||||
[HostsTableType.uncommonProcesses]: 'UncommonProcessQueryTabBody',
|
||||
[HostsTableType.anomalies]: 'AnomaliesQueryTabBody',
|
||||
[HostsTableType.events]: 'EventsQueryTabBody',
|
||||
|
|
|
@ -20,7 +20,6 @@ import type { HostDetailsTabsProps } from './types';
|
|||
import { type } from './utils';
|
||||
|
||||
import {
|
||||
HostsQueryTabBody,
|
||||
AuthenticationsQueryTabBody,
|
||||
UncommonProcessQueryTabBody,
|
||||
HostRiskTabBody,
|
||||
|
@ -62,9 +61,6 @@ export const HostDetailsTabs = React.memo<HostDetailsTabsProps>(
|
|||
<Route path={`${hostDetailsPagePath}/:tabName(${HostsTableType.authentications})`}>
|
||||
<AuthenticationsQueryTabBody {...tabProps} />
|
||||
</Route>
|
||||
<Route path={`${hostDetailsPagePath}/:tabName(${HostsTableType.hosts})`}>
|
||||
<HostsQueryTabBody {...tabProps} />
|
||||
</Route>
|
||||
<Route path={`${hostDetailsPagePath}/:tabName(${HostsTableType.uncommonProcesses})`}>
|
||||
<UncommonProcessQueryTabBody {...tabProps} />
|
||||
</Route>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants';
|
||||
import { from, mockAlertsData, alertsByStatusQuery, parsedMockAlertsData, to } from './mock_data';
|
||||
import type { UseAlertsByStatus, UseAlertsByStatusProps } from './use_alerts_by_status';
|
||||
import { useAlertsByStatus } from './use_alerts_by_status';
|
||||
|
@ -72,6 +73,7 @@ describe('useAlertsByStatus', () => {
|
|||
query: alertsByStatusQuery,
|
||||
indexName: 'signal-alerts',
|
||||
skip: false,
|
||||
queryName: ALERTS_QUERY_NAMES.BY_STATUS,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -117,6 +119,7 @@ describe('useAlertsByStatus', () => {
|
|||
query: alertsByStatusQuery,
|
||||
indexName: 'signal-alerts',
|
||||
skip: true,
|
||||
queryName: ALERTS_QUERY_NAMES.BY_STATUS,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
|
|
|
@ -9,6 +9,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants';
|
||||
import { useQueryInspector } from '../../../../common/components/page/manage_query';
|
||||
import type { AlertsByStatusAgg, AlertsByStatusResponse, ParsedAlertsData } from './types';
|
||||
import {
|
||||
|
@ -109,6 +110,7 @@ export const useAlertsByStatus: UseAlertsByStatus = ({
|
|||
}),
|
||||
indexName: signalIndexName,
|
||||
skip,
|
||||
queryName: ALERTS_QUERY_NAMES.BY_STATUS,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants';
|
||||
import { buildVulnerableHostAggregationQuery } from './use_host_alerts_items';
|
||||
|
||||
export const mockVulnerableHostsBySeverityResult = {
|
||||
|
@ -126,4 +127,5 @@ export const mockQuery = () => ({
|
|||
}),
|
||||
indexName: 'signal-alerts',
|
||||
skip: false,
|
||||
queryName: ALERTS_QUERY_NAMES.VULNERABLE_HOSTS,
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useQueryInspector } from '../../../../common/components/page/manage_que
|
|||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import type { GenericBuckets } from '../../../../../common/search_strategy';
|
||||
import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants';
|
||||
import { getPageCount, ITEMS_PER_PAGE } from '../utils';
|
||||
|
||||
const HOSTS_BY_SEVERITY_AGG = 'hostsBySeverity';
|
||||
|
@ -73,6 +74,7 @@ export const useHostAlertsItems: UseHostAlertsItems = ({ skip, queryId, signalIn
|
|||
}),
|
||||
indexName: signalIndexName,
|
||||
skip,
|
||||
queryName: ALERTS_QUERY_NAMES.VULNERABLE_HOSTS,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants';
|
||||
|
||||
import {
|
||||
from,
|
||||
|
@ -74,6 +75,7 @@ describe('useRuleAlertsItems', () => {
|
|||
query: severityRuleAlertsQuery,
|
||||
indexName: 'signal-alerts',
|
||||
skip: false,
|
||||
queryName: ALERTS_QUERY_NAMES.BY_SEVERITY,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -118,6 +120,7 @@ describe('useRuleAlertsItems', () => {
|
|||
query: severityRuleAlertsQuery,
|
||||
indexName: 'signal-alerts',
|
||||
skip: true,
|
||||
queryName: ALERTS_QUERY_NAMES.BY_SEVERITY,
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
|
|
|
@ -9,6 +9,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants';
|
||||
import { useQueryInspector } from '../../../../common/components/page/manage_query';
|
||||
|
||||
// Formatted item result
|
||||
|
@ -136,6 +137,7 @@ export const useRuleAlertsItems: UseRuleAlertsItems = ({
|
|||
}),
|
||||
indexName: signalIndexName,
|
||||
skip,
|
||||
queryName: ALERTS_QUERY_NAMES.BY_SEVERITY,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants';
|
||||
import { buildVulnerableUserAggregationQuery } from './use_user_alerts_items';
|
||||
|
||||
export const mockVulnerableUsersBySeverityResult = {
|
||||
|
@ -126,4 +127,5 @@ export const mockQuery = () => ({
|
|||
}),
|
||||
indexName: 'signal-alerts',
|
||||
skip: false,
|
||||
queryName: ALERTS_QUERY_NAMES.VULNERABLE_USERS,
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useQueryInspector } from '../../../../common/components/page/manage_que
|
|||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import type { GenericBuckets } from '../../../../../common/search_strategy';
|
||||
import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query';
|
||||
import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants';
|
||||
import { getPageCount, ITEMS_PER_PAGE } from '../utils';
|
||||
|
||||
const USERS_BY_SEVERITY_AGG = 'usersBySeverity';
|
||||
|
@ -73,6 +74,7 @@ export const useUserAlertsItems: UseUserAlertsItems = ({ skip, queryId, signalIn
|
|||
}),
|
||||
indexName: signalIndexName,
|
||||
skip,
|
||||
queryName: ALERTS_QUERY_NAMES.VULNERABLE_USERS,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import type { DataView } from '@kbn/data-views-plugin/public';
|
|||
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
clearEventsLoading,
|
||||
clearEventsDeleted,
|
||||
|
@ -42,6 +43,7 @@ import type { KueryFilterQueryKind } from '../../common/types/timeline';
|
|||
import { useAppToasts } from '../hooks/use_app_toasts';
|
||||
import { TimelineId } from '../store/t_grid/types';
|
||||
import * as i18n from './translations';
|
||||
import { TimelinesStartPlugins } from '../types';
|
||||
|
||||
export type InspectResponse = Inspect & { response: string[] };
|
||||
|
||||
|
@ -115,6 +117,30 @@ export const initSortDefault = [
|
|||
},
|
||||
];
|
||||
|
||||
const useApmTracking = (timelineId: string) => {
|
||||
const { apm } = useKibana<TimelinesStartPlugins>().services;
|
||||
|
||||
const startTracking = useCallback(() => {
|
||||
// Create the transaction, the managed flag is turned off to prevent it from being polluted by non-related automatic spans.
|
||||
// The managed flag can be turned on to investigate high latency requests in APM.
|
||||
// However, note that by enabling the managed flag, the transaction trace may be distorted by other requests information.
|
||||
const transaction = apm?.startTransaction(`Timeline search ${timelineId}`, 'http-request', {
|
||||
managed: false,
|
||||
});
|
||||
// Create a blocking span to control the transaction time and prevent it from closing automatically with partial batch responses.
|
||||
// The blocking span needs to be ended manually when the batched request finishes.
|
||||
const span = transaction?.startSpan('batched search', 'http-request', { blocking: true });
|
||||
return {
|
||||
endTracking: (result: 'success' | 'error' | 'aborted' | 'invalid') => {
|
||||
transaction?.addLabels({ result });
|
||||
span?.end();
|
||||
},
|
||||
};
|
||||
}, [apm, timelineId]);
|
||||
|
||||
return { startTracking };
|
||||
};
|
||||
|
||||
const NO_CONSUMERS: AlertConsumers[] = [];
|
||||
export const useTimelineEvents = ({
|
||||
alertConsumers = NO_CONSUMERS,
|
||||
|
@ -136,6 +162,7 @@ export const useTimelineEvents = ({
|
|||
data,
|
||||
}: UseTimelineEventsProps): [boolean, TimelineArgs] => {
|
||||
const dispatch = useDispatch();
|
||||
const { startTracking } = useApmTracking(id);
|
||||
const refetch = useRef<Refetch>(noop);
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
const searchSubscription$ = useRef(new Subscription());
|
||||
|
@ -205,6 +232,9 @@ export const useTimelineEvents = ({
|
|||
abortCtrl.current = new AbortController();
|
||||
setLoading(true);
|
||||
if (data && data.search) {
|
||||
const { endTracking } = startTracking();
|
||||
const abortSignal = abortCtrl.current.signal;
|
||||
|
||||
searchSubscription$.current = data.search
|
||||
.search<TimelineRequest<typeof language>, TimelineResponse<typeof language>>(
|
||||
{ ...request, entityType },
|
||||
|
@ -213,7 +243,7 @@ export const useTimelineEvents = ({
|
|||
request.language === 'eql'
|
||||
? 'timelineEqlSearchStrategy'
|
||||
: 'timelineSearchStrategy',
|
||||
abortSignal: abortCtrl.current.signal,
|
||||
abortSignal,
|
||||
// we only need the id to throw better errors
|
||||
indexPattern: { id: dataViewId } as unknown as DataView,
|
||||
}
|
||||
|
@ -221,6 +251,7 @@ export const useTimelineEvents = ({
|
|||
.subscribe({
|
||||
next: (response) => {
|
||||
if (isCompleteResponse(response)) {
|
||||
endTracking('success');
|
||||
setTimelineResponse((prevResponse) => {
|
||||
const newTimelineResponse = {
|
||||
...prevResponse,
|
||||
|
@ -238,12 +269,14 @@ export const useTimelineEvents = ({
|
|||
|
||||
searchSubscription$.current.unsubscribe();
|
||||
} else if (isErrorResponse(response)) {
|
||||
endTracking('invalid');
|
||||
setLoading(false);
|
||||
addWarning(i18n.ERROR_TIMELINE_EVENTS);
|
||||
searchSubscription$.current.unsubscribe();
|
||||
}
|
||||
},
|
||||
error: (msg) => {
|
||||
endTracking(abortSignal.aborted ? 'aborted' : 'error');
|
||||
setLoading(false);
|
||||
data.search.showError(msg);
|
||||
searchSubscription$.current.unsubscribe();
|
||||
|
@ -257,7 +290,7 @@ export const useTimelineEvents = ({
|
|||
asyncSearch();
|
||||
refetch.current = asyncSearch;
|
||||
},
|
||||
[skip, data, entityType, dataViewId, setUpdated, addWarning]
|
||||
[skip, data, entityType, dataViewId, setUpdated, addWarning, startTracking]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { CoreStart } from '@kbn/core/public';
|
|||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { CasesUiStart } from '@kbn/cases-plugin/public';
|
||||
import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { ApmBase } from '@elastic/apm-rum';
|
||||
import type {
|
||||
LastUpdatedAtProps,
|
||||
LoadingPanelProps,
|
||||
|
@ -46,6 +47,7 @@ export interface TimelinesStartPlugins {
|
|||
data: DataPublicPluginStart;
|
||||
cases: CasesUiStart;
|
||||
triggersActionsUi: TriggersActionsStart;
|
||||
apm?: ApmBase;
|
||||
}
|
||||
|
||||
export type TimelinesStartServices = CoreStart & TimelinesStartPlugins;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue