mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] Improve explore pages performance (#178893)
## Summary
issue: https://github.com/elastic/kibana/issues/178372
- Split Network flow queries, extracting the cardinality aggregation
from the data aggregation, resulting in one query for the totalCount
aggregation, and another one for the IPs data aggregation, that are
executed in parallel.
- Fixed search strategy observable bug, causing duplicate request to be
sent for every query
Screenshots:

---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
0bad865382
commit
d1e792a5a0
23 changed files with 495 additions and 234 deletions
|
@ -35,7 +35,7 @@ import {
|
|||
import { networkOverviewSchema } from './network/overview';
|
||||
import { networkTlsSchema } from './network/tls';
|
||||
import { networkTopCountriesSchema } from './network/top_countries';
|
||||
import { networkTopNFlowSchema } from './network/top_n_flow';
|
||||
import { networkTopNFlowCountSchema, networkTopNFlowSchema } from './network/top_n_flow';
|
||||
import { networkUsersSchema } from './network/users';
|
||||
|
||||
import {
|
||||
|
@ -106,6 +106,7 @@ export const searchStrategyRequestSchema = z.discriminatedUnion('factoryQueryTyp
|
|||
networkTlsSchema,
|
||||
networkTopCountriesSchema,
|
||||
networkTopNFlowSchema,
|
||||
networkTopNFlowCountSchema,
|
||||
networkUsersSchema,
|
||||
networkKpiDns,
|
||||
networkKpiEvents,
|
||||
|
|
|
@ -41,6 +41,7 @@ export enum NetworkQueries {
|
|||
overview = 'overviewNetwork',
|
||||
tls = 'tls',
|
||||
topCountries = 'topCountries',
|
||||
topNFlowCount = 'topNFlowCount',
|
||||
topNFlow = 'topNFlow',
|
||||
users = 'users',
|
||||
}
|
||||
|
|
|
@ -7,13 +7,14 @@
|
|||
|
||||
import { z } from 'zod';
|
||||
import { NetworkQueries } from '../model/factory_query_type';
|
||||
import { requestBasicOptionsSchema } from '../model/request_basic_options';
|
||||
import { requestOptionsPaginatedSchema } from '../model/request_paginated_options';
|
||||
import { sort } from '../model/sort';
|
||||
import { timerange } from '../model/timerange';
|
||||
import { flowTarget } from './model/flow_target';
|
||||
|
||||
export const networkTopNFlowSchema = requestOptionsPaginatedSchema.extend({
|
||||
ip: z.string().ip().nullable().optional(),
|
||||
ip: z.string().ip().nullish(),
|
||||
flowTarget,
|
||||
sort,
|
||||
timerange,
|
||||
|
@ -21,5 +22,13 @@ export const networkTopNFlowSchema = requestOptionsPaginatedSchema.extend({
|
|||
});
|
||||
|
||||
export type NetworkTopNFlowRequestOptionsInput = z.input<typeof networkTopNFlowSchema>;
|
||||
|
||||
export type NetworkTopNFlowRequestOptions = z.infer<typeof networkTopNFlowSchema>;
|
||||
|
||||
export const networkTopNFlowCountSchema = requestBasicOptionsSchema.extend({
|
||||
ip: z.string().ip().nullish(),
|
||||
flowTarget,
|
||||
timerange,
|
||||
factoryQueryType: z.literal(NetworkQueries.topNFlowCount),
|
||||
});
|
||||
export type NetworkTopNFlowCountRequestOptionsInput = z.input<typeof networkTopNFlowCountSchema>;
|
||||
export type NetworkTopNFlowCountRequestOptions = z.infer<typeof networkTopNFlowCountSchema>;
|
||||
|
|
|
@ -31,6 +31,7 @@ import type {
|
|||
NetworkKpiTlsHandshakesStrategyResponse,
|
||||
NetworkKpiUniqueFlowsStrategyResponse,
|
||||
NetworkKpiUniquePrivateIpsStrategyResponse,
|
||||
NetworkTopNFlowCountStrategyResponse,
|
||||
} from './network';
|
||||
import type { MatrixHistogramQuery, MatrixHistogramStrategyResponse } from './matrix_histogram';
|
||||
import type {
|
||||
|
@ -104,6 +105,8 @@ import type {
|
|||
NetworkTlsRequestOptionsInput,
|
||||
NetworkTopCountriesRequestOptions,
|
||||
NetworkTopCountriesRequestOptionsInput,
|
||||
NetworkTopNFlowCountRequestOptions,
|
||||
NetworkTopNFlowCountRequestOptionsInput,
|
||||
NetworkTopNFlowRequestOptions,
|
||||
NetworkTopNFlowRequestOptionsInput,
|
||||
NetworkUsersRequestOptions,
|
||||
|
@ -189,6 +192,8 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
|
|||
? NetworkTopCountriesStrategyResponse
|
||||
: T extends NetworkQueries.topNFlow
|
||||
? NetworkTopNFlowStrategyResponse
|
||||
: T extends NetworkQueries.topNFlowCount
|
||||
? NetworkTopNFlowCountStrategyResponse
|
||||
: T extends NetworkQueries.users
|
||||
? NetworkUsersStrategyResponse
|
||||
: T extends NetworkKpiQueries.dns
|
||||
|
@ -259,6 +264,8 @@ export type StrategyRequestInputType<T extends FactoryQueryTypes> = T extends Ho
|
|||
? NetworkTopCountriesRequestOptionsInput
|
||||
: T extends NetworkQueries.topNFlow
|
||||
? NetworkTopNFlowRequestOptionsInput
|
||||
: T extends NetworkQueries.topNFlowCount
|
||||
? NetworkTopNFlowCountRequestOptionsInput
|
||||
: T extends NetworkQueries.users
|
||||
? NetworkUsersRequestOptionsInput
|
||||
: T extends NetworkKpiQueries.dns
|
||||
|
@ -329,6 +336,8 @@ export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQu
|
|||
? NetworkTopCountriesRequestOptions
|
||||
: T extends NetworkQueries.topNFlow
|
||||
? NetworkTopNFlowRequestOptions
|
||||
: T extends NetworkQueries.topNFlowCount
|
||||
? NetworkTopNFlowCountRequestOptions
|
||||
: T extends NetworkQueries.users
|
||||
? NetworkUsersRequestOptions
|
||||
: T extends NetworkKpiQueries.dns
|
||||
|
|
|
@ -7,19 +7,15 @@
|
|||
|
||||
import type { IEsSearchResponse } from '@kbn/data-plugin/common';
|
||||
import type { GeoItem, TopNetworkTablesEcsField } from '../common';
|
||||
import type {
|
||||
CursorType,
|
||||
Inspect,
|
||||
Maybe,
|
||||
PageInfoPaginated,
|
||||
TotalValue,
|
||||
GenericBuckets,
|
||||
} from '../../../common';
|
||||
import type { CursorType, Inspect, Maybe, TotalValue, GenericBuckets } from '../../../common';
|
||||
|
||||
export interface NetworkTopNFlowStrategyResponse extends IEsSearchResponse {
|
||||
edges: NetworkTopNFlowEdges[];
|
||||
inspect?: Maybe<Inspect>;
|
||||
}
|
||||
|
||||
export interface NetworkTopNFlowCountStrategyResponse extends IEsSearchResponse {
|
||||
totalCount: number;
|
||||
pageInfo: PageInfoPaginated;
|
||||
inspect?: Maybe<Inspect>;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,9 +13,10 @@ import type {
|
|||
FactoryQueryTypes,
|
||||
StrategyRequestInputType,
|
||||
} from '../../../../common/search_strategy';
|
||||
import { Observable } from 'rxjs';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
jest.mock('@kbn/securitysolution-hook-utils');
|
||||
|
||||
const mockAddToastError = jest.fn();
|
||||
const mockAddToastWarning = jest.fn();
|
||||
jest.mock('../../hooks/use_app_toasts', () => ({
|
||||
|
@ -25,20 +26,13 @@ jest.mock('../../hooks/use_app_toasts', () => ({
|
|||
})),
|
||||
}));
|
||||
|
||||
// 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());
|
||||
})
|
||||
const mockSearch = jest.fn(() =>
|
||||
// default to completed response
|
||||
of({
|
||||
rawResponse: {},
|
||||
isPartial: false,
|
||||
isRunning: false,
|
||||
})
|
||||
);
|
||||
jest.mock('../../lib/kibana', () => {
|
||||
const original = jest.requireActual('../../lib/kibana');
|
||||
|
@ -269,7 +263,7 @@ describe('useSearchStrategy', () => {
|
|||
describe('search function', () => {
|
||||
it('should track successful search result', () => {
|
||||
const { result } = renderHook(() => useSearch<FactoryQueryTypes>(factoryQueryType));
|
||||
result.current({ request, abortSignal: new AbortController().signal });
|
||||
result.current({ request, abortSignal: new AbortController().signal }).subscribe();
|
||||
|
||||
expect(mockStartTracking).toBeCalledTimes(1);
|
||||
expect(mockEndTracking).toBeCalledTimes(1);
|
||||
|
@ -277,24 +271,25 @@ describe('useSearchStrategy', () => {
|
|||
});
|
||||
|
||||
it('should handle search error', () => {
|
||||
mockResponse.mockImplementation(() => {
|
||||
throw new Error('simulated search error');
|
||||
const error = 'simulated search error';
|
||||
mockSearch.mockImplementationOnce(() => {
|
||||
return throwError(() => Error(error));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSearch<FactoryQueryTypes>(factoryQueryType));
|
||||
result.current({ request, abortSignal: new AbortController().signal });
|
||||
result.current({ request, abortSignal: new AbortController().signal }).subscribe();
|
||||
|
||||
expect(mockStartTracking).toBeCalledTimes(1);
|
||||
expect(mockEndTracking).toBeCalledWith('error');
|
||||
});
|
||||
|
||||
it('should track error search result', () => {
|
||||
mockResponse.mockImplementationOnce(() => {
|
||||
throw Error('fake server error');
|
||||
mockSearch.mockImplementationOnce(() => {
|
||||
return throwError(() => Error('fake server error'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSearch<FactoryQueryTypes>(factoryQueryType));
|
||||
result.current({ request, abortSignal: new AbortController().signal });
|
||||
result.current({ request, abortSignal: new AbortController().signal }).subscribe();
|
||||
|
||||
expect(mockStartTracking).toBeCalledTimes(1);
|
||||
expect(mockEndTracking).toBeCalledTimes(1);
|
||||
|
@ -303,13 +298,13 @@ describe('useSearchStrategy', () => {
|
|||
|
||||
it('should track aborted search result', () => {
|
||||
const abortController = new AbortController();
|
||||
mockResponse.mockImplementationOnce(() => {
|
||||
mockSearch.mockImplementationOnce(() => {
|
||||
abortController.abort();
|
||||
throw Error('fake aborted');
|
||||
return throwError(() => Error('fake aborted'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSearch<FactoryQueryTypes>(factoryQueryType));
|
||||
result.current({ request, abortSignal: abortController.signal });
|
||||
result.current({ request, abortSignal: abortController.signal }).subscribe();
|
||||
|
||||
expect(mockStartTracking).toBeCalledTimes(1);
|
||||
expect(mockEndTracking).toBeCalledTimes(1);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { filter } from 'rxjs';
|
||||
import { catchError, filter, tap } from 'rxjs';
|
||||
import { noop, omit } from 'lodash/fp';
|
||||
import { useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import type { Observable } from 'rxjs';
|
||||
|
@ -55,27 +55,22 @@ export const useSearch = <QueryType extends FactoryQueryTypes>(
|
|||
name: `${APP_UI_ID} searchStrategy ${factoryQueryType}`,
|
||||
spanName: 'batched search',
|
||||
});
|
||||
|
||||
const observable = data.search
|
||||
// return observable directly, any extra subscription causes duplicate requests
|
||||
return data.search
|
||||
.search<StrategyRequestInputType<QueryType>, StrategyResponseType<QueryType>>(
|
||||
{ ...request, factoryQueryType } as StrategyRequestInputType<QueryType>,
|
||||
{
|
||||
strategy: 'securitySolutionSearchStrategy',
|
||||
abortSignal,
|
||||
}
|
||||
{ strategy: 'securitySolutionSearchStrategy', abortSignal }
|
||||
)
|
||||
.pipe(filter((response) => !isRunningResponse(response)));
|
||||
|
||||
observable.subscribe({
|
||||
next: (response) => {
|
||||
endTracking('success');
|
||||
},
|
||||
error: () => {
|
||||
endTracking(abortSignal.aborted ? 'aborted' : 'error');
|
||||
},
|
||||
});
|
||||
|
||||
return observable;
|
||||
.pipe(
|
||||
filter((response) => !isRunningResponse(response)),
|
||||
catchError((error) => {
|
||||
endTracking(abortSignal.aborted ? 'aborted' : 'error');
|
||||
throw error;
|
||||
}),
|
||||
tap(() => {
|
||||
endTracking('success');
|
||||
})
|
||||
);
|
||||
},
|
||||
[data.search, factoryQueryType, startTracking]
|
||||
);
|
||||
|
@ -131,10 +126,7 @@ export const useSearchStrategy = <QueryType extends FactoryQueryTypes>({
|
|||
(request) => {
|
||||
const startSearch = () => {
|
||||
abortCtrl.current = new AbortController();
|
||||
start({
|
||||
request,
|
||||
abortSignal: abortCtrl.current.signal,
|
||||
});
|
||||
start({ request, abortSignal: abortCtrl.current.signal });
|
||||
};
|
||||
|
||||
abortCtrl.current.abort();
|
||||
|
|
|
@ -9,6 +9,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta
|
|||
loading={false}
|
||||
>
|
||||
<HeaderSectionComponent
|
||||
inspectMultiple={true}
|
||||
subtitle="Showing: 1 Test Unit"
|
||||
title="Hosts"
|
||||
toggleQuery={[Function]}
|
||||
|
|
|
@ -16,7 +16,45 @@ export const generateTablePaginationOptions = (
|
|||
return {
|
||||
activePage,
|
||||
cursorStart,
|
||||
fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5,
|
||||
fakePossibleCount: getFakePossibleCount(activePage, limit),
|
||||
querySize: isBucketSort ? limit : limit + cursorStart,
|
||||
};
|
||||
};
|
||||
|
||||
export const getLimitedPaginationOptions = (
|
||||
activePage: number,
|
||||
limit: number
|
||||
): PaginationInputPaginatedInput => {
|
||||
const cursorStart = activePage * limit;
|
||||
return {
|
||||
activePage,
|
||||
cursorStart,
|
||||
querySize: limit + cursorStart,
|
||||
// TODO: Limited pagination behavior is UI-only logic, the server API should not have to know anything about it.
|
||||
// Remove this parameter from the API schema when all security solution requests are updated.
|
||||
fakePossibleCount: 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const getLimitedPaginationTotalCount = ({
|
||||
activePage,
|
||||
limit,
|
||||
totalCount,
|
||||
}: {
|
||||
activePage: number;
|
||||
limit: number;
|
||||
totalCount: number;
|
||||
}): number => {
|
||||
const fakePossibleCount = getFakePossibleCount(activePage, limit);
|
||||
return fakePossibleCount <= totalCount ? fakePossibleCount : totalCount;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function returns a fake possible count based on the active page and limit.
|
||||
* The goal is to restring the pagination to prevent querying arbitrary pages, which may cause performance issues.
|
||||
* After initializing the table we allow the user to navigate to pages 1-5.
|
||||
* If the user reaches page 5 or higher, we only allow to go to the following page.
|
||||
*/
|
||||
const getFakePossibleCount = (activePage: number, limit: number): number => {
|
||||
return activePage < 4 ? limit * 5 : limit * (activePage + 2);
|
||||
};
|
||||
|
|
|
@ -117,8 +117,7 @@ export interface BasicTableProps<T> {
|
|||
loading: boolean;
|
||||
loadPage: (activePage: number) => void;
|
||||
onChange?: (criteria: Criteria) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pageOfItems: any[];
|
||||
pageOfItems: unknown[];
|
||||
setQuerySkip: (skip: boolean) => void;
|
||||
showMorePagesIndicator: boolean;
|
||||
sorting?: SortingBasicTable;
|
||||
|
@ -292,6 +291,7 @@ const PaginatedTableComponent: FC<SiemTables> = ({
|
|||
}
|
||||
title={headerTitle}
|
||||
tooltip={headerTooltip}
|
||||
inspectMultiple
|
||||
>
|
||||
{!loadingInitial && headerSupplement}
|
||||
</HeaderSection>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { Provider as ReduxStoreProvider } from 'react-redux';
|
||||
|
||||
|
@ -15,7 +14,7 @@ import { TestProviders, createMockStore } from '../../../../common/mock';
|
|||
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
|
||||
import { networkModel } from '../../store';
|
||||
import { NetworkTopNFlowTable } from '.';
|
||||
import { mockData } from './mock';
|
||||
import { mockData, mockCount } from './mock';
|
||||
import { FlowTargetSourceDest } from '../../../../../common/search_strategy';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
@ -27,15 +26,15 @@ describe('NetworkTopNFlow Table Component', () => {
|
|||
const mount = useMountAppended();
|
||||
const defaultProps = {
|
||||
data: mockData.edges,
|
||||
fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.pageInfo),
|
||||
fakeTotalCount: 50,
|
||||
flowTargeted: FlowTargetSourceDest.source,
|
||||
id: 'topNFlowSource',
|
||||
isInspect: false,
|
||||
loading: false,
|
||||
loadPage,
|
||||
setQuerySkip: jest.fn(),
|
||||
showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockData.pageInfo),
|
||||
totalCount: mockData.totalCount,
|
||||
showMorePagesIndicator: true,
|
||||
totalCount: mockCount.totalCount,
|
||||
type: networkModel.NetworkType.page,
|
||||
};
|
||||
|
||||
|
|
|
@ -22,17 +22,16 @@ import { PaginatedTable } from '../../../components/paginated_table';
|
|||
import { networkActions, networkModel, networkSelectors } from '../../store';
|
||||
import { getNFlowColumnsCurated } from './columns';
|
||||
import * as i18n from './translations';
|
||||
import { getLimitedPaginationTotalCount } from '../../../components/paginated_table/helpers';
|
||||
|
||||
interface NetworkTopNFlowTableProps {
|
||||
data: NetworkTopNFlowEdges[];
|
||||
fakeTotalCount: number;
|
||||
flowTargeted: FlowTargetSourceDest;
|
||||
id: string;
|
||||
isInspect: boolean;
|
||||
loading: boolean;
|
||||
loadPage: (newActivePage: number) => void;
|
||||
setQuerySkip: (skip: boolean) => void;
|
||||
showMorePagesIndicator: boolean;
|
||||
totalCount: number;
|
||||
type: networkModel.NetworkType;
|
||||
}
|
||||
|
@ -52,14 +51,12 @@ export const NetworkTopNFlowTableId = 'networkTopSourceFlow-top-talkers';
|
|||
|
||||
const NetworkTopNFlowTableComponent: React.FC<NetworkTopNFlowTableProps> = ({
|
||||
data,
|
||||
fakeTotalCount,
|
||||
flowTargeted,
|
||||
id,
|
||||
isInspect,
|
||||
loading,
|
||||
loadPage,
|
||||
setQuerySkip,
|
||||
showMorePagesIndicator,
|
||||
totalCount,
|
||||
type,
|
||||
}) => {
|
||||
|
@ -152,6 +149,16 @@ const NetworkTopNFlowTableComponent: React.FC<NetworkTopNFlowTableProps> = ({
|
|||
[dispatch, type, tableType]
|
||||
);
|
||||
|
||||
// limits pagination to 5 pages with 1 page increments to prevent expensive queries
|
||||
const limitedPaginationTotalCount = useMemo(
|
||||
() => getLimitedPaginationTotalCount({ activePage, totalCount, limit }),
|
||||
[activePage, limit, totalCount]
|
||||
);
|
||||
|
||||
const showMorePagesIndicator = useMemo(() => {
|
||||
return totalCount / limit > limitedPaginationTotalCount / limit;
|
||||
}, [totalCount, limitedPaginationTotalCount, limit]);
|
||||
|
||||
return (
|
||||
<PaginatedTable
|
||||
activePage={activePage}
|
||||
|
@ -171,7 +178,7 @@ const NetworkTopNFlowTableComponent: React.FC<NetworkTopNFlowTableProps> = ({
|
|||
setQuerySkip={setQuerySkip}
|
||||
showMorePagesIndicator={showMorePagesIndicator}
|
||||
sorting={sorting}
|
||||
totalCount={fakeTotalCount}
|
||||
totalCount={limitedPaginationTotalCount}
|
||||
updateActivePage={updateActivePage}
|
||||
updateLimitPagination={updateLimitPagination}
|
||||
/>
|
||||
|
|
|
@ -5,11 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { NetworkTopNFlowStrategyResponse } from '../../../../../common/search_strategy';
|
||||
import type {
|
||||
NetworkTopNFlowCountStrategyResponse,
|
||||
NetworkTopNFlowStrategyResponse,
|
||||
} from '../../../../../common/search_strategy';
|
||||
import { FlowTargetSourceDest } from '../../../../../common/search_strategy';
|
||||
|
||||
export const mockData: NetworkTopNFlowStrategyResponse = {
|
||||
export const mockCount: NetworkTopNFlowCountStrategyResponse = {
|
||||
totalCount: 524,
|
||||
rawResponse: {} as NetworkTopNFlowStrategyResponse['rawResponse'],
|
||||
};
|
||||
|
||||
export const mockData: NetworkTopNFlowStrategyResponse = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
|
@ -74,10 +81,5 @@ export const mockData: NetworkTopNFlowStrategyResponse = {
|
|||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
activePage: 1,
|
||||
fakeTotalCount: 50,
|
||||
showMorePagesIndicator: true,
|
||||
},
|
||||
rawResponse: {} as NetworkTopNFlowStrategyResponse['rawResponse'],
|
||||
};
|
||||
|
|
|
@ -36,15 +36,10 @@ describe('useNetworkTopNFlow', () => {
|
|||
result: {
|
||||
edges: [],
|
||||
totalCount: -1,
|
||||
pageInfo: {
|
||||
activePage: 0,
|
||||
fakeTotalCount: 0,
|
||||
showMorePagesIndicator: false,
|
||||
},
|
||||
},
|
||||
search: mockSearch,
|
||||
refetch: jest.fn(),
|
||||
inspect: {},
|
||||
inspect: { dsl: [], response: [] },
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -77,7 +72,11 @@ describe('useNetworkTopNFlow', () => {
|
|||
});
|
||||
localProps.skip = true;
|
||||
act(() => rerender());
|
||||
expect(mockUseSearchStrategy).toHaveBeenCalledTimes(3);
|
||||
expect(mockUseSearchStrategy.mock.calls[2][0].abort).toEqual(true);
|
||||
|
||||
// there are 2 calls inside the hook, 3 renders for each call
|
||||
expect(mockUseSearchStrategy).toHaveBeenCalledTimes(6);
|
||||
// last two calls are the ones that are aborted
|
||||
expect(mockUseSearchStrategy.mock.calls[4][0].abort).toEqual(true);
|
||||
expect(mockUseSearchStrategy.mock.calls[5][0].abort).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,13 +13,12 @@ import type { ESTermQuery } from '../../../../../common/typed_json';
|
|||
import type { inputsModel } from '../../../../common/store';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { createFilter } from '../../../../common/containers/helpers';
|
||||
import { generateTablePaginationOptions } from '../../../components/paginated_table/helpers';
|
||||
import { getLimitedPaginationOptions } from '../../../components/paginated_table/helpers';
|
||||
import type { networkModel } from '../../store';
|
||||
import { networkSelectors } from '../../store';
|
||||
import type {
|
||||
FlowTargetSourceDest,
|
||||
NetworkTopNFlowEdges,
|
||||
PageInfoPaginated,
|
||||
} from '../../../../../common/search_strategy';
|
||||
import { NetworkQueries } from '../../../../../common/search_strategy';
|
||||
import type { InspectResponse } from '../../../../types';
|
||||
|
@ -33,13 +32,12 @@ export interface NetworkTopNFlowArgs {
|
|||
inspect: InspectResponse;
|
||||
isInspected: boolean;
|
||||
loadPage: (newActivePage: number) => void;
|
||||
pageInfo: PageInfoPaginated;
|
||||
refetch: inputsModel.Refetch;
|
||||
networkTopNFlow: NetworkTopNFlowEdges[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
interface UseNetworkTopNFlow {
|
||||
interface UseNetworkTopNFlowProps {
|
||||
flowTarget: FlowTargetSourceDest;
|
||||
id: string;
|
||||
ip?: string;
|
||||
|
@ -61,7 +59,7 @@ export const useNetworkTopNFlow = ({
|
|||
skip,
|
||||
startDate,
|
||||
type,
|
||||
}: UseNetworkTopNFlow): [boolean, NetworkTopNFlowArgs] => {
|
||||
}: UseNetworkTopNFlowProps): [boolean, NetworkTopNFlowArgs] => {
|
||||
const getTopNFlowSelector = useMemo(() => networkSelectors.topNFlowSelector(), []);
|
||||
const { activePage, limit, sort } = useDeepEqualSelector((state) =>
|
||||
getTopNFlowSelector(state, type, flowTarget)
|
||||
|
@ -70,16 +68,15 @@ export const useNetworkTopNFlow = ({
|
|||
const [networkTopNFlowRequest, setTopNFlowRequest] =
|
||||
useState<NetworkTopNFlowRequestOptionsInput | null>(null);
|
||||
|
||||
const wrappedLoadMore = useCallback(
|
||||
const loadPage = useCallback(
|
||||
(newActivePage: number) => {
|
||||
setTopNFlowRequest((prevRequest) => {
|
||||
if (!prevRequest) {
|
||||
return prevRequest;
|
||||
}
|
||||
|
||||
return {
|
||||
...prevRequest,
|
||||
pagination: generateTablePaginationOptions(newActivePage, limit),
|
||||
pagination: getLimitedPaginationOptions(newActivePage, limit),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
@ -87,49 +84,61 @@ export const useNetworkTopNFlow = ({
|
|||
);
|
||||
|
||||
const {
|
||||
loading,
|
||||
loading: isLoadingData,
|
||||
result: response,
|
||||
search,
|
||||
refetch,
|
||||
refetch: refetchData,
|
||||
inspect,
|
||||
} = useSearchStrategy<NetworkQueries.topNFlow>({
|
||||
factoryQueryType: NetworkQueries.topNFlow,
|
||||
initialResult: {
|
||||
edges: [],
|
||||
totalCount: -1,
|
||||
pageInfo: {
|
||||
activePage: 0,
|
||||
fakeTotalCount: 0,
|
||||
showMorePagesIndicator: false,
|
||||
},
|
||||
},
|
||||
initialResult: { edges: [] },
|
||||
errorMessage: i18n.FAIL_NETWORK_TOP_N_FLOW,
|
||||
abort: skip,
|
||||
});
|
||||
|
||||
const {
|
||||
loading: isLoadingTotalCount,
|
||||
result: responseTotalCount,
|
||||
search: searchTotalCount,
|
||||
refetch: refetchTotalCount,
|
||||
inspect: inspectTotalCount,
|
||||
} = useSearchStrategy<NetworkQueries.topNFlowCount>({
|
||||
factoryQueryType: NetworkQueries.topNFlowCount,
|
||||
initialResult: { totalCount: -1 },
|
||||
errorMessage: i18n.FAIL_NETWORK_TOP_N_FLOW,
|
||||
abort: skip,
|
||||
});
|
||||
|
||||
const isLoading = isLoadingData || isLoadingTotalCount;
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
refetchData();
|
||||
refetchTotalCount();
|
||||
}, [refetchData, refetchTotalCount]);
|
||||
|
||||
const networkTopNFlowResponse = useMemo(
|
||||
() => ({
|
||||
endDate,
|
||||
networkTopNFlow: response.edges,
|
||||
id,
|
||||
inspect,
|
||||
inspect: {
|
||||
dsl: [...inspect.dsl, ...inspectTotalCount.dsl],
|
||||
response: [...inspect.response, ...inspectTotalCount.response],
|
||||
},
|
||||
isInspected: false,
|
||||
loadPage: wrappedLoadMore,
|
||||
pageInfo: response.pageInfo,
|
||||
loadPage,
|
||||
refetch,
|
||||
startDate,
|
||||
totalCount: response.totalCount,
|
||||
totalCount: responseTotalCount.totalCount,
|
||||
}),
|
||||
[
|
||||
endDate,
|
||||
id,
|
||||
inspect,
|
||||
inspectTotalCount,
|
||||
refetch,
|
||||
response.edges,
|
||||
response.pageInfo,
|
||||
response.totalCount,
|
||||
responseTotalCount.totalCount,
|
||||
startDate,
|
||||
wrappedLoadMore,
|
||||
loadPage,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -142,7 +151,7 @@ export const useNetworkTopNFlow = ({
|
|||
filterQuery: createFilter(filterQuery),
|
||||
flowTarget,
|
||||
ip,
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
pagination: getLimitedPaginationOptions(activePage, limit),
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
from: startDate,
|
||||
|
@ -160,8 +169,9 @@ export const useNetworkTopNFlow = ({
|
|||
useEffect(() => {
|
||||
if (!skip && networkTopNFlowRequest) {
|
||||
search(networkTopNFlowRequest);
|
||||
searchTotalCount(networkTopNFlowRequest);
|
||||
}
|
||||
}, [networkTopNFlowRequest, search, skip]);
|
||||
}, [networkTopNFlowRequest, search, searchTotalCount, skip]);
|
||||
|
||||
return [loading, networkTopNFlowResponse];
|
||||
return [isLoading, networkTopNFlowResponse];
|
||||
};
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getOr } from 'lodash/fp';
|
||||
|
||||
import { NetworkTopNFlowTable } from '../../components/network_top_n_flow_table';
|
||||
import { ID, useNetworkTopNFlow } from '../../containers/network_top_n_flow';
|
||||
|
@ -34,25 +33,23 @@ export const IPsQueryTabBody = ({
|
|||
useEffect(() => {
|
||||
setQuerySkip(skip || !toggleStatus);
|
||||
}, [skip, toggleStatus]);
|
||||
const [
|
||||
loading,
|
||||
{ id, inspect, isInspected, loadPage, networkTopNFlow, pageInfo, refetch, totalCount },
|
||||
] = useNetworkTopNFlow({
|
||||
endDate,
|
||||
flowTarget,
|
||||
filterQuery,
|
||||
id: queryId,
|
||||
indexNames,
|
||||
ip,
|
||||
skip: querySkip,
|
||||
startDate,
|
||||
type,
|
||||
});
|
||||
|
||||
const [loading, { id, inspect, isInspected, loadPage, networkTopNFlow, refetch, totalCount }] =
|
||||
useNetworkTopNFlow({
|
||||
endDate,
|
||||
flowTarget,
|
||||
filterQuery,
|
||||
id: queryId,
|
||||
indexNames,
|
||||
ip,
|
||||
skip: querySkip,
|
||||
startDate,
|
||||
type,
|
||||
});
|
||||
|
||||
return (
|
||||
<NetworkTopNFlowTableManage
|
||||
data={networkTopNFlow}
|
||||
fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
|
||||
flowTargeted={flowTarget}
|
||||
id={id}
|
||||
inspect={inspect}
|
||||
|
@ -62,7 +59,6 @@ export const IPsQueryTabBody = ({
|
|||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
setQuerySkip={setQuerySkip}
|
||||
showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)}
|
||||
totalCount={totalCount}
|
||||
type={type}
|
||||
/>
|
||||
|
|
|
@ -21,7 +21,7 @@ import { networkHttp } from './http';
|
|||
import { networkOverview } from './overview';
|
||||
import { networkTls } from './tls';
|
||||
import { networkTopCountries } from './top_countries';
|
||||
import { networkTopNFlow } from './top_n_flow';
|
||||
import { networkTopNFlow, networkTopNFlowCount } from './top_n_flow';
|
||||
import { networkUsers } from './users';
|
||||
|
||||
// TODO: add safer type for the strategy map
|
||||
|
@ -33,6 +33,7 @@ export const networkFactory: Record<NetworkQueries | NetworkKpiQueries, any> = {
|
|||
[NetworkQueries.overview]: networkOverview,
|
||||
[NetworkQueries.tls]: networkTls,
|
||||
[NetworkQueries.topCountries]: networkTopCountries,
|
||||
[NetworkQueries.topNFlowCount]: networkTopNFlowCount,
|
||||
[NetworkQueries.topNFlow]: networkTopNFlow,
|
||||
[NetworkQueries.users]: networkUsers,
|
||||
[NetworkKpiQueries.dns]: networkKpiDns,
|
||||
|
|
|
@ -6,9 +6,15 @@
|
|||
*/
|
||||
|
||||
import type { IEsSearchResponse } from '@kbn/data-plugin/common';
|
||||
import type { NetworkTopNFlowRequestOptions } from '../../../../../../../common/api/search_strategy';
|
||||
import type {
|
||||
NetworkTopNFlowCountRequestOptions,
|
||||
NetworkTopNFlowRequestOptions,
|
||||
} from '../../../../../../../common/api/search_strategy';
|
||||
|
||||
import type { NetworkTopNFlowStrategyResponse } from '../../../../../../../common/search_strategy';
|
||||
import type {
|
||||
NetworkTopNFlowCountStrategyResponse,
|
||||
NetworkTopNFlowStrategyResponse,
|
||||
} from '../../../../../../../common/search_strategy';
|
||||
import {
|
||||
Direction,
|
||||
FlowTargetSourceDest,
|
||||
|
@ -35,6 +41,23 @@ export const mockOptions: NetworkTopNFlowRequestOptions = {
|
|||
timerange: { interval: '12h', from: '2020-09-13T10:16:46.870Z', to: '2020-09-14T10:16:46.870Z' },
|
||||
};
|
||||
|
||||
export const mockCountOptions: NetworkTopNFlowCountRequestOptions = {
|
||||
defaultIndex: [
|
||||
'apm-*-transaction*',
|
||||
'traces-apm*',
|
||||
'auditbeat-*',
|
||||
'endgame-*',
|
||||
'filebeat-*',
|
||||
'logs-*',
|
||||
'packetbeat-*',
|
||||
'winlogbeat-*',
|
||||
],
|
||||
factoryQueryType: NetworkQueries.topNFlowCount,
|
||||
filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}',
|
||||
flowTarget: FlowTargetSourceDest.source,
|
||||
timerange: { interval: '12h', from: '2020-09-13T10:16:46.870Z', to: '2020-09-14T10:16:46.870Z' },
|
||||
};
|
||||
|
||||
export const mockSearchStrategyResponse: IEsSearchResponse<unknown> = {
|
||||
isPartial: false,
|
||||
isRunning: false,
|
||||
|
@ -563,6 +586,20 @@ export const mockSearchStrategyResponse: IEsSearchResponse<unknown> = {
|
|||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
total: 21,
|
||||
loaded: 21,
|
||||
};
|
||||
export const mockCountStrategyResponse: IEsSearchResponse<unknown> = {
|
||||
isPartial: false,
|
||||
isRunning: false,
|
||||
rawResponse: {
|
||||
took: 191,
|
||||
timed_out: false,
|
||||
_shards: { total: 21, successful: 21, skipped: 0, failed: 0 },
|
||||
hits: { max_score: 0, hits: [], total: 0 },
|
||||
aggregations: {
|
||||
top_n_flow_count: { value: 738 },
|
||||
},
|
||||
},
|
||||
|
@ -837,7 +874,6 @@ export const formattedSearchStrategyResponse: NetworkTopNFlowStrategyResponse =
|
|||
ignore_unavailable: true,
|
||||
body: {
|
||||
aggregations: {
|
||||
top_n_flow_count: { cardinality: { field: 'source.ip' } },
|
||||
source: {
|
||||
terms: { field: 'source.ip', size: 10, order: { bytes_out: 'desc' } },
|
||||
aggs: {
|
||||
|
@ -920,7 +956,56 @@ export const formattedSearchStrategyResponse: NetworkTopNFlowStrategyResponse =
|
|||
),
|
||||
],
|
||||
},
|
||||
pageInfo: { activePage: 0, fakeTotalCount: 50, showMorePagesIndicator: true },
|
||||
rawResponse: {} as NetworkTopNFlowStrategyResponse['rawResponse'],
|
||||
};
|
||||
|
||||
export const formattedCountStrategyResponse: NetworkTopNFlowCountStrategyResponse = {
|
||||
inspect: {
|
||||
dsl: [
|
||||
JSON.stringify(
|
||||
{
|
||||
allow_no_indices: true,
|
||||
index: [
|
||||
'apm-*-transaction*',
|
||||
'traces-apm*',
|
||||
'auditbeat-*',
|
||||
'endgame-*',
|
||||
'filebeat-*',
|
||||
'logs-*',
|
||||
'packetbeat-*',
|
||||
'winlogbeat-*',
|
||||
],
|
||||
ignore_unavailable: true,
|
||||
body: {
|
||||
aggregations: {
|
||||
top_n_flow_count: { cardinality: { field: 'source.ip' } },
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ bool: { must: [], filter: [{ match_all: {} }], should: [], must_not: [] } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: '2020-09-13T10:16:46.870Z',
|
||||
lte: '2020-09-14T10:16:46.870Z',
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
_source: false,
|
||||
},
|
||||
size: 0,
|
||||
track_total_hits: false,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
],
|
||||
},
|
||||
totalCount: 738,
|
||||
rawResponse: {} as NetworkTopNFlowStrategyResponse['rawResponse'],
|
||||
};
|
||||
|
@ -940,7 +1025,6 @@ export const expectedDsl = {
|
|||
ignore_unavailable: true,
|
||||
body: {
|
||||
aggregations: {
|
||||
top_n_flow_count: { cardinality: { field: 'source.ip' } },
|
||||
source: {
|
||||
terms: { field: 'source.ip', size: 10, order: { bytes_out: 'desc' } },
|
||||
aggs: {
|
||||
|
@ -1018,3 +1102,42 @@ export const expectedDsl = {
|
|||
size: 0,
|
||||
track_total_hits: false,
|
||||
};
|
||||
|
||||
export const expectedCountDsl = {
|
||||
allow_no_indices: true,
|
||||
index: [
|
||||
'apm-*-transaction*',
|
||||
'traces-apm*',
|
||||
'auditbeat-*',
|
||||
'endgame-*',
|
||||
'filebeat-*',
|
||||
'logs-*',
|
||||
'packetbeat-*',
|
||||
'winlogbeat-*',
|
||||
],
|
||||
ignore_unavailable: true,
|
||||
body: {
|
||||
aggregations: {
|
||||
top_n_flow_count: { cardinality: { field: 'source.ip' } },
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ bool: { must: [], filter: [{ match_all: {} }], should: [], must_not: [] } },
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: '2020-09-13T10:16:46.870Z',
|
||||
lte: '2020-09-14T10:16:46.870Z',
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
_source: false,
|
||||
},
|
||||
size: 0,
|
||||
track_total_hits: false,
|
||||
};
|
||||
|
|
|
@ -6,31 +6,61 @@
|
|||
*/
|
||||
|
||||
import * as buildQuery from './query.top_n_flow_network.dsl';
|
||||
import { networkTopNFlow } from '.';
|
||||
import { networkTopNFlow, networkTopNFlowCount } from '.';
|
||||
import {
|
||||
mockOptions,
|
||||
mockCountOptions,
|
||||
mockSearchStrategyResponse,
|
||||
formattedSearchStrategyResponse,
|
||||
mockCountStrategyResponse,
|
||||
formattedCountStrategyResponse,
|
||||
} from './__mocks__';
|
||||
|
||||
describe('networkTopNFlow search strategy', () => {
|
||||
const buildTopNFlowQuery = jest.spyOn(buildQuery, 'buildTopNFlowQuery');
|
||||
describe('Network TopNFlow search strategy', () => {
|
||||
describe('networkTopNFlow', () => {
|
||||
const buildTopNFlowQuery = jest.spyOn(buildQuery, 'buildTopNFlowQuery');
|
||||
|
||||
afterEach(() => {
|
||||
buildTopNFlowQuery.mockClear();
|
||||
});
|
||||
afterEach(() => {
|
||||
buildTopNFlowQuery.mockClear();
|
||||
});
|
||||
|
||||
describe('buildDsl', () => {
|
||||
test('should build dsl query', () => {
|
||||
networkTopNFlow.buildDsl(mockOptions);
|
||||
expect(buildTopNFlowQuery).toHaveBeenCalledWith(mockOptions);
|
||||
describe('buildDsl', () => {
|
||||
test('should build dsl query', () => {
|
||||
networkTopNFlow.buildDsl(mockOptions);
|
||||
expect(buildTopNFlowQuery).toHaveBeenCalledWith(mockOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
test('should parse data correctly', async () => {
|
||||
const result = await networkTopNFlow.parse(mockOptions, mockSearchStrategyResponse);
|
||||
expect(result).toMatchObject(formattedSearchStrategyResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
test('should parse data correctly', async () => {
|
||||
const result = await networkTopNFlow.parse(mockOptions, mockSearchStrategyResponse);
|
||||
expect(result).toMatchObject(formattedSearchStrategyResponse);
|
||||
describe('networkTopNFlowCount', () => {
|
||||
const buildTopNFlowCountQuery = jest.spyOn(buildQuery, 'buildTopNFlowCountQuery');
|
||||
|
||||
afterEach(() => {
|
||||
buildTopNFlowCountQuery.mockClear();
|
||||
});
|
||||
|
||||
describe('buildDsl', () => {
|
||||
test('should build dsl query', () => {
|
||||
networkTopNFlowCount.buildDsl(mockCountOptions);
|
||||
expect(buildTopNFlowCountQuery).toHaveBeenCalledWith(mockCountOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
test('should parse data correctly', async () => {
|
||||
const result = await networkTopNFlowCount.parse(
|
||||
mockCountOptions,
|
||||
mockCountStrategyResponse
|
||||
);
|
||||
expect(result).toMatchObject(formattedCountStrategyResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,13 +14,14 @@ import type {
|
|||
NetworkTopNFlowStrategyResponse,
|
||||
NetworkQueries,
|
||||
NetworkTopNFlowEdges,
|
||||
NetworkTopNFlowCountStrategyResponse,
|
||||
} from '../../../../../../common/search_strategy/security_solution/network';
|
||||
|
||||
import { inspectStringifyObject } from '../../../../../utils/build_query';
|
||||
import type { SecuritySolutionFactory } from '../../types';
|
||||
|
||||
import { getTopNFlowEdges } from './helpers';
|
||||
import { buildTopNFlowQuery } from './query.top_n_flow_network.dsl';
|
||||
import { buildTopNFlowQuery, buildTopNFlowCountQuery } from './query.top_n_flow_network.dsl';
|
||||
|
||||
export const networkTopNFlow: SecuritySolutionFactory<NetworkQueries.topNFlow> = {
|
||||
buildDsl: (options) => {
|
||||
|
@ -33,26 +34,24 @@ export const networkTopNFlow: SecuritySolutionFactory<NetworkQueries.topNFlow> =
|
|||
options,
|
||||
response: IEsSearchResponse<unknown>
|
||||
): Promise<NetworkTopNFlowStrategyResponse> => {
|
||||
const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination;
|
||||
const totalCount = getOr(0, 'aggregations.top_n_flow_count.value', response.rawResponse);
|
||||
const { cursorStart, querySize } = options.pagination;
|
||||
const networkTopNFlowEdges: NetworkTopNFlowEdges[] = getTopNFlowEdges(response, options);
|
||||
const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount;
|
||||
const edges = networkTopNFlowEdges.splice(cursorStart, querySize - cursorStart);
|
||||
const inspect = {
|
||||
dsl: [inspectStringifyObject(buildTopNFlowQuery(options))],
|
||||
};
|
||||
const showMorePagesIndicator = totalCount > fakeTotalCount;
|
||||
|
||||
return {
|
||||
...response,
|
||||
edges,
|
||||
inspect,
|
||||
pageInfo: {
|
||||
activePage: activePage ?? 0,
|
||||
fakeTotalCount,
|
||||
showMorePagesIndicator,
|
||||
},
|
||||
totalCount,
|
||||
};
|
||||
const inspect = { dsl: [inspectStringifyObject(buildTopNFlowQuery(options))] };
|
||||
return { ...response, inspect, edges };
|
||||
},
|
||||
};
|
||||
|
||||
export const networkTopNFlowCount: SecuritySolutionFactory<NetworkQueries.topNFlowCount> = {
|
||||
buildDsl: (options) => buildTopNFlowCountQuery(options),
|
||||
parse: async (
|
||||
options,
|
||||
response: IEsSearchResponse<unknown>
|
||||
): Promise<NetworkTopNFlowCountStrategyResponse> => {
|
||||
const totalCount = getOr(0, 'rawResponse.aggregations.top_n_flow_count.value', response);
|
||||
|
||||
const inspect = { dsl: [inspectStringifyObject(buildTopNFlowCountQuery(options))] };
|
||||
return { ...response, inspect, totalCount };
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,11 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { buildTopNFlowQuery } from './query.top_n_flow_network.dsl';
|
||||
import { mockOptions, expectedDsl } from './__mocks__';
|
||||
import { buildTopNFlowCountQuery, buildTopNFlowQuery } from './query.top_n_flow_network.dsl';
|
||||
import { mockOptions, mockCountOptions, expectedDsl, expectedCountDsl } from './__mocks__';
|
||||
|
||||
describe('buildTopNFlowQuery', () => {
|
||||
test('build query from options correctly', () => {
|
||||
expect(buildTopNFlowQuery(mockOptions)).toEqual(expectedDsl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTopNFlowCountQuery', () => {
|
||||
test('build query from options correctly', () => {
|
||||
expect(buildTopNFlowCountQuery(mockCountOptions)).toEqual(expectedCountDsl);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,19 +5,30 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { NetworkTopNFlowRequestOptions } from '../../../../../../common/api/search_strategy';
|
||||
import type {
|
||||
AggregationsAggregationContainer,
|
||||
AggregationsTopHitsAggregation,
|
||||
Field,
|
||||
QueryDslFieldAndFormat,
|
||||
QueryDslQueryContainer,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { ISearchRequestParams } from '@kbn/data-plugin/common';
|
||||
import type {
|
||||
NetworkTopNFlowCountRequestOptions,
|
||||
NetworkTopNFlowRequestOptions,
|
||||
} from '../../../../../../common/api/search_strategy';
|
||||
import type { FlowTargetSourceDest } from '../../../../../../common/search_strategy';
|
||||
import { createQueryFilterClauses } from '../../../../../utils/build_query';
|
||||
import { getOppositeField } from '../helpers';
|
||||
import { getQueryOrder } from './helpers';
|
||||
|
||||
const getCountAgg = (flowTarget: FlowTargetSourceDest) => ({
|
||||
top_n_flow_count: {
|
||||
cardinality: {
|
||||
field: `${flowTarget}.ip`,
|
||||
},
|
||||
},
|
||||
});
|
||||
interface AggregationsAggregationWithFieldsContainer extends AggregationsAggregationContainer {
|
||||
aggregations?: Record<string, AggregationsAggregationWithFieldsContainer>;
|
||||
aggs?: Record<string, AggregationsAggregationWithFieldsContainer>;
|
||||
top_hits?: AggregationsTopHitsAggregation & {
|
||||
fields?: Array<QueryDslFieldAndFormat | Field>; // fields is missing in the official types but it is used in the query
|
||||
};
|
||||
}
|
||||
|
||||
export const buildTopNFlowQuery = ({
|
||||
defaultIndex,
|
||||
|
@ -25,46 +36,19 @@ export const buildTopNFlowQuery = ({
|
|||
flowTarget,
|
||||
sort,
|
||||
pagination,
|
||||
timerange: { from, to },
|
||||
timerange,
|
||||
ip,
|
||||
}: NetworkTopNFlowRequestOptions) => {
|
||||
}: NetworkTopNFlowRequestOptions): ISearchRequestParams => {
|
||||
const querySize = pagination?.querySize ?? 10;
|
||||
|
||||
const filter = [
|
||||
...createQueryFilterClauses(filterQuery),
|
||||
{
|
||||
range: {
|
||||
'@timestamp': { gte: from, lte: to, format: 'strict_date_optional_time' },
|
||||
},
|
||||
},
|
||||
];
|
||||
const query = getQuery({ filterQuery, flowTarget, timerange, ip });
|
||||
|
||||
const dslQuery = {
|
||||
allow_no_indices: true,
|
||||
index: defaultIndex,
|
||||
ignore_unavailable: true,
|
||||
body: {
|
||||
aggregations: {
|
||||
...getCountAgg(flowTarget),
|
||||
...getFlowTargetAggs(sort, flowTarget, querySize),
|
||||
},
|
||||
query: {
|
||||
bool: ip
|
||||
? {
|
||||
filter,
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
[`${getOppositeField(flowTarget)}.ip`]: ip,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
}
|
||||
: {
|
||||
filter,
|
||||
},
|
||||
},
|
||||
aggregations: getFlowTargetAggs(sort, flowTarget, querySize),
|
||||
query,
|
||||
_source: false,
|
||||
fields: [
|
||||
{
|
||||
|
@ -79,11 +63,61 @@ export const buildTopNFlowQuery = ({
|
|||
return dslQuery;
|
||||
};
|
||||
|
||||
export const buildTopNFlowCountQuery = ({
|
||||
defaultIndex,
|
||||
filterQuery,
|
||||
flowTarget,
|
||||
timerange,
|
||||
ip,
|
||||
}: NetworkTopNFlowCountRequestOptions): ISearchRequestParams => {
|
||||
const query = getQuery({ filterQuery, flowTarget, timerange, ip });
|
||||
const dslQuery = {
|
||||
allow_no_indices: true,
|
||||
index: defaultIndex,
|
||||
ignore_unavailable: true,
|
||||
body: { aggregations: getCountAgg(flowTarget), query, _source: false },
|
||||
size: 0,
|
||||
track_total_hits: false,
|
||||
};
|
||||
return dslQuery;
|
||||
};
|
||||
|
||||
// creates the dsl bool query with the filters
|
||||
const getQuery = ({
|
||||
filterQuery,
|
||||
flowTarget,
|
||||
timerange: { from, to },
|
||||
ip,
|
||||
}: Pick<
|
||||
NetworkTopNFlowRequestOptions,
|
||||
'filterQuery' | 'flowTarget' | 'timerange' | 'ip'
|
||||
>): QueryDslQueryContainer => ({
|
||||
bool: {
|
||||
filter: [...createQueryFilterClauses(filterQuery), getTimeRangeFilter(from, to)],
|
||||
...(ip && {
|
||||
should: [{ term: { [`${getOppositeField(flowTarget)}.ip`]: ip } }],
|
||||
minimum_should_match: 1,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const getTimeRangeFilter = (from: string, to: string) => ({
|
||||
range: {
|
||||
'@timestamp': { gte: from, lte: to, format: 'strict_date_optional_time' },
|
||||
},
|
||||
});
|
||||
|
||||
const getCountAgg = (
|
||||
flowTarget: FlowTargetSourceDest
|
||||
): Record<string, AggregationsAggregationContainer> => ({
|
||||
top_n_flow_count: { cardinality: { field: `${flowTarget}.ip` } },
|
||||
});
|
||||
|
||||
const getFlowTargetAggs = (
|
||||
sort: NetworkTopNFlowRequestOptions['sort'],
|
||||
flowTarget: FlowTargetSourceDest,
|
||||
querySize: number
|
||||
) => ({
|
||||
): Record<string, AggregationsAggregationWithFieldsContainer> => ({
|
||||
[flowTarget]: {
|
||||
terms: {
|
||||
field: `${flowTarget}.ip`,
|
||||
|
|
|
@ -36,7 +36,27 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const FROM = '2019-02-09T01:57:24.870Z';
|
||||
const TO = '2019-02-12T01:57:24.870Z';
|
||||
|
||||
it('Make sure that we get Source NetworkTopNFlow data with bytes_in descending sort', async () => {
|
||||
it('should get Source NetworkTopNFlowCount total count', async () => {
|
||||
const networkTopNFlow = await bsearch.send<NetworkTopNFlowStrategyResponse>({
|
||||
supertest,
|
||||
options: {
|
||||
defaultIndex: ['filebeat-*'],
|
||||
factoryQueryType: NetworkQueries.topNFlowCount,
|
||||
flowTarget: FlowTargetSourceDest.source,
|
||||
timerange: {
|
||||
interval: '12h',
|
||||
to: TO,
|
||||
from: FROM,
|
||||
},
|
||||
inspect: false,
|
||||
},
|
||||
strategy: 'securitySolutionSearchStrategy',
|
||||
});
|
||||
|
||||
expect(networkTopNFlow.totalCount).to.be(121);
|
||||
});
|
||||
|
||||
it('should get Source NetworkTopNFlow data with bytes_in descending sort', async () => {
|
||||
const networkTopNFlow = await bsearch.send<NetworkTopNFlowStrategyResponse>({
|
||||
supertest,
|
||||
options: {
|
||||
|
@ -47,7 +67,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
pagination: {
|
||||
activePage: 0,
|
||||
cursorStart: 0,
|
||||
fakePossibleCount: 50,
|
||||
fakePossibleCount: 0,
|
||||
querySize: 10,
|
||||
},
|
||||
timerange: {
|
||||
|
@ -61,7 +81,6 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
expect(networkTopNFlow.edges.length).to.be(EDGE_LENGTH);
|
||||
expect(networkTopNFlow.totalCount).to.be(121);
|
||||
expect(
|
||||
networkTopNFlow.edges.map((i: NetworkTopNFlowEdges) => i.node.source!.ip).join(',')
|
||||
).to.be(
|
||||
|
@ -70,15 +89,14 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(networkTopNFlow.edges[0].node.destination).to.be(undefined);
|
||||
expect(networkTopNFlow.edges[0].node.source!.flows).to.be(498);
|
||||
expect(networkTopNFlow.edges[0].node.source!.destination_ips).to.be(132);
|
||||
expect(networkTopNFlow.pageInfo.fakeTotalCount).to.equal(50);
|
||||
});
|
||||
|
||||
it('Make sure that we get Source NetworkTopNFlow data with bytes_in ascending sort ', async () => {
|
||||
it('should get Source NetworkTopNFlow data with bytes_in ascending sort ', async () => {
|
||||
const networkTopNFlow = await bsearch.send<NetworkTopNFlowStrategyResponse>({
|
||||
supertest,
|
||||
options: {
|
||||
defaultIndex: ['filebeat-*'],
|
||||
factoryQueryType: 'topNFlow',
|
||||
factoryQueryType: NetworkQueries.topNFlow,
|
||||
filterQuery:
|
||||
'{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}',
|
||||
flowTarget: FlowTargetSourceDest.source,
|
||||
|
@ -86,7 +104,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
pagination: {
|
||||
activePage: 0,
|
||||
cursorStart: 0,
|
||||
fakePossibleCount: 50,
|
||||
fakePossibleCount: 0,
|
||||
querySize: 10,
|
||||
},
|
||||
timerange: {
|
||||
|
@ -100,7 +118,6 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
expect(networkTopNFlow.edges.length).to.be(EDGE_LENGTH);
|
||||
expect(networkTopNFlow.totalCount).to.be(121);
|
||||
expect(
|
||||
networkTopNFlow.edges.map((i: NetworkTopNFlowEdges) => i.node.source!.ip).join(',')
|
||||
).to.be(
|
||||
|
@ -109,10 +126,9 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(networkTopNFlow.edges[0].node.destination).to.be(undefined);
|
||||
expect(networkTopNFlow.edges[0].node.source!.flows).to.be(12);
|
||||
expect(networkTopNFlow.edges[0].node.source!.destination_ips).to.be(1);
|
||||
expect(networkTopNFlow.pageInfo.fakeTotalCount).to.equal(50);
|
||||
});
|
||||
|
||||
it('Make sure that we get Destination NetworkTopNFlow data', async () => {
|
||||
it('should get Destination NetworkTopNFlow data', async () => {
|
||||
const networkTopNFlow = await bsearch.send<NetworkTopNFlowStrategyResponse>({
|
||||
supertest,
|
||||
options: {
|
||||
|
@ -125,7 +141,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
pagination: {
|
||||
activePage: 0,
|
||||
cursorStart: 0,
|
||||
fakePossibleCount: 50,
|
||||
fakePossibleCount: 0,
|
||||
querySize: 10,
|
||||
},
|
||||
timerange: {
|
||||
|
@ -139,14 +155,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
expect(networkTopNFlow.edges.length).to.be(EDGE_LENGTH);
|
||||
expect(networkTopNFlow.totalCount).to.be(154);
|
||||
expect(networkTopNFlow.edges[0].node.destination!.flows).to.be(19);
|
||||
expect(networkTopNFlow.edges[0].node.destination!.source_ips).to.be(1);
|
||||
expect(networkTopNFlow.edges[0].node.source).to.be(undefined);
|
||||
expect(networkTopNFlow.pageInfo.fakeTotalCount).to.equal(50);
|
||||
});
|
||||
|
||||
it('Make sure that pagination is working in NetworkTopNFlow query', async () => {
|
||||
it('should paginate NetworkTopNFlow query', async () => {
|
||||
const networkTopNFlow = await bsearch.send<NetworkTopNFlowStrategyResponse>({
|
||||
supertest,
|
||||
options: {
|
||||
|
@ -159,7 +173,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
pagination: {
|
||||
activePage: 1,
|
||||
cursorStart: 10,
|
||||
fakePossibleCount: 50,
|
||||
fakePossibleCount: 0,
|
||||
querySize: 20,
|
||||
},
|
||||
timerange: {
|
||||
|
@ -173,7 +187,6 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
expect(networkTopNFlow.edges.length).to.be(EDGE_LENGTH);
|
||||
expect(networkTopNFlow.totalCount).to.be(121);
|
||||
expect(networkTopNFlow.edges[0].node.source!.ip).to.be('8.248.223.246');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue