From 8003bc014430bca9e20eb69c971b2da691a1aa29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Thu, 3 Sep 2020 18:06:31 +0200 Subject: [PATCH] [Security Solution] Refactor Network Top Countries to use Search Strategy (#76244) --- .../security_solution/index.ts | 6 + .../security_solution/network/common/index.ts | 10 + .../security_solution/network/index.ts | 5 +- .../security_solution/network/tls/index.ts | 6 +- .../network/top_countries/index.ts | 98 ++++++ .../link_to/redirect_to_network.tsx | 5 +- .../public/common/components/links/index.tsx | 5 +- .../public/hosts/containers/hosts/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 4 +- .../network_top_countries_table/columns.tsx | 2 +- .../index.test.tsx | 6 +- .../network_top_countries_table/index.tsx | 234 +++++++------- .../network_top_countries_table/mock.ts | 5 +- .../network_top_countries/index.tsx | 295 ++++++++++-------- .../network_top_countries/translations.ts | 21 ++ .../network_top_countries_query_table.tsx | 76 ++--- .../public/network/pages/ip_details/types.ts | 5 +- .../navigation/countries_query_tab_body.tsx | 74 ++--- .../pages/navigation/network_routes.tsx | 2 +- .../public/network/pages/navigation/types.ts | 2 +- .../public/network/store/selectors.ts | 2 +- .../factory/network/helpers.ts | 18 ++ .../factory/network/index.ts | 2 + .../factory/network/top_countries/helpers.ts | 53 ++++ .../factory/network/top_countries/index.ts | 62 ++++ .../query.top_countries_network.dsl.ts | 152 +++++++++ 26 files changed, 800 insertions(+), 353 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts create mode 100644 x-pack/plugins/security_solution/public/network/containers/network_top_countries/translations.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/helpers.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/helpers.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/query.top_countries_network.dsl.ts diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 474002c93f24f7..d87ce42ab1418f 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -25,6 +25,8 @@ import { NetworkTlsRequestOptions, NetworkHttpStrategyResponse, NetworkHttpRequestOptions, + NetworkTopCountriesStrategyResponse, + NetworkTopCountriesRequestOptions, } from './network'; export * from './hosts'; @@ -168,6 +170,8 @@ export type StrategyResponseType = T extends HostsQ ? NetworkTlsStrategyResponse : T extends NetworkQueries.http ? NetworkHttpStrategyResponse + : T extends NetworkQueries.topCountries + ? NetworkTopCountriesStrategyResponse : never; export type StrategyRequestType = T extends HostsQueries.hosts @@ -182,6 +186,8 @@ export type StrategyRequestType = T extends HostsQu ? NetworkTlsRequestOptions : T extends NetworkQueries.http ? NetworkHttpRequestOptions + : T extends NetworkQueries.topCountries + ? NetworkTopCountriesRequestOptions : never; export type StringOrNumber = string | number; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts new file mode 100644 index 00000000000000..a6ae956a421877 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum FlowTargetSourceDest { + destination = 'destination', + source = 'source', +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts index 194bb5d057e3fc..ac5e6fdacc94b9 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './tls'; +export * from './common'; export * from './http'; +export * from './tls'; +export * from './top_countries'; export enum NetworkQueries { http = 'http', tls = 'tls', + topCountries = 'topCountries', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts index c9e593bb7a7d2d..b1d30c3d4f9bf3 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts @@ -6,6 +6,7 @@ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; import { CursorType, Inspect, Maybe, PageInfoPaginated, RequestOptionsPaginated } from '../..'; +import { FlowTargetSourceDest } from '../common'; export interface TlsBuckets { key: string; @@ -36,11 +37,6 @@ export interface TlsNode { issuers?: Maybe; } -export enum FlowTargetSourceDest { - destination = 'destination', - source = 'source', -} - export enum TlsFields { _id = '_id', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts new file mode 100644 index 00000000000000..6d514d12519c39 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { GeoEcs } from '../../../../ecs/geo'; +import { CursorType, Inspect, Maybe, PageInfoPaginated, RequestOptionsPaginated } from '../..'; +import { FlowTargetSourceDest } from '../common'; + +export enum NetworkTopTablesFields { + bytes_in = 'bytes_in', + bytes_out = 'bytes_out', + flows = 'flows', + destination_ips = 'destination_ips', + source_ips = 'source_ips', +} + +export enum NetworkDnsFields { + dnsName = 'dnsName', + queryCount = 'queryCount', + uniqueDomains = 'uniqueDomains', + dnsBytesIn = 'dnsBytesIn', + dnsBytesOut = 'dnsBytesOut', +} + +export enum FlowTarget { + client = 'client', + destination = 'destination', + server = 'server', + source = 'source', +} + +export interface GeoItem { + geo?: Maybe; + flowTarget?: Maybe; +} + +export interface TopCountriesItemSource { + country?: Maybe; + destination_ips?: Maybe; + flows?: Maybe; + location?: Maybe; + source_ips?: Maybe; +} + +export interface NetworkTopCountriesRequestOptions + extends RequestOptionsPaginated { + flowTarget: FlowTargetSourceDest; + ip?: string; +} + +export interface NetworkTopCountriesStrategyResponse extends IEsSearchResponse { + edges: NetworkTopCountriesEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface NetworkTopCountriesEdges { + node: NetworkTopCountriesItem; + cursor: CursorType; +} + +export interface NetworkTopCountriesItem { + _id?: Maybe; + source?: Maybe; + destination?: Maybe; + network?: Maybe; +} + +export interface TopCountriesItemDestination { + country?: Maybe; + destination_ips?: Maybe; + flows?: Maybe; + location?: Maybe; + source_ips?: Maybe; +} + +export interface TopNetworkTablesEcsField { + bytes_in?: Maybe; + bytes_out?: Maybe; +} + +export interface NetworkTopCountriesBuckets { + country: string; + key: string; + bytes_in: { + value: number; + }; + bytes_out: { + value: number; + }; + flows: number; + destination_ips: number; + source_ips: number; +} diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx index 8e2b47bd91dbc4..100c5e46141a20 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; +import { + FlowTarget, + FlowTargetSourceDest, +} from '../../../../common/search_strategy/security_solution/network'; import { appendSearch } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 2f7aa1b14cfda9..943f2d8336ca7f 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -32,7 +32,10 @@ import { getCreateCaseUrl, useFormatUrl, } from '../link_to'; -import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; +import { + FlowTarget, + FlowTargetSourceDest, +} from '../../../../common/search_strategy/security_solution/network'; import { useUiSetting$, useKibana } from '../../lib/kibana'; import { isUrlInvalid } from '../../utils/validators'; import { ExternalLinkIcon } from '../external_link_icon'; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index c545a7a75457bd..74748e5399b781 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -10,13 +10,14 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { HostsEdges, PageInfoPaginated } from '../../../graphql/types'; import { inputsModel, State } from '../../../common/store'; import { createFilter } from '../../../common/containers/helpers'; import { useKibana } from '../../../common/lib/kibana'; import { hostsModel, hostsSelectors } from '../../store'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; import { + HostsEdges, + PageInfoPaginated, DocValueFields, HostsQueries, HostsRequestOptions, diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap index 1127528c776b75..02a8802bfced16 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`NetworkTopCountries Table Component rendering it renders the IP Details NetworkTopCountries table 1`] = ` - { ); - expect(wrapper.find('Connect(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); + expect(wrapper.find('Memo(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); }); test('it renders the IP Details NetworkTopCountries table', () => { const wrapper = shallow( @@ -101,7 +101,7 @@ describe('NetworkTopCountries Table Component', () => { ); - expect(wrapper.find('Connect(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); + expect(wrapper.find('Memo(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx index 93d3f410ddde40..dfd93caf25394c 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx @@ -6,7 +6,7 @@ import { last } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { IIndexPattern } from 'src/plugins/data/public'; @@ -16,8 +16,8 @@ import { FlowTargetSourceDest, NetworkTopCountriesEdges, NetworkTopTablesFields, - NetworkTopTablesSortField, -} from '../../../graphql/types'; + SortField, +} from '../../../../common/search_strategy/security_solution'; import { State } from '../../../common/store'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; @@ -25,7 +25,7 @@ import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/component import { getCountriesColumnsCurated } from './columns'; import * as i18n from './translations'; -interface OwnProps { +interface NetworkTopCountriesTableProps { data: NetworkTopCountriesEdges[]; fakeTotalCount: number; flowTargeted: FlowTargetSourceDest; @@ -39,8 +39,6 @@ interface OwnProps { type: networkModel.NetworkType; } -type NetworkTopCountriesTableProps = OwnProps & PropsFromRedux; - const rowItems: ItemsPerRow[] = [ { text: i18n.ROWS_5, @@ -54,139 +52,133 @@ const rowItems: ItemsPerRow[] = [ export const NetworkTopCountriesTableId = 'networkTopCountries-top-talkers'; -const NetworkTopCountriesTableComponent = React.memo( - ({ - activePage, - data, - fakeTotalCount, - flowTargeted, - id, - indexPattern, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - sort, - totalCount, - type, - updateNetworkTable, - }) => { - let tableType: networkModel.TopCountriesTableType; - const headerTitle: string = +const NetworkTopCountriesTableComponent: React.FC = ({ + data, + fakeTotalCount, + flowTargeted, + id, + indexPattern, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const getTopCountriesSelector = networkSelectors.topCountriesSelector(); + const { activePage, limit, sort } = useSelector( + (state: State) => getTopCountriesSelector(state, type, flowTargeted), + shallowEqual + ); + + const headerTitle: string = useMemo( + () => flowTargeted === FlowTargetSourceDest.source ? i18n.SOURCE_COUNTRIES - : i18n.DESTINATION_COUNTRIES; + : i18n.DESTINATION_COUNTRIES, + [flowTargeted] + ); + const tableType: networkModel.TopCountriesTableType = useMemo(() => { if (type === networkModel.NetworkType.page) { - tableType = - flowTargeted === FlowTargetSourceDest.source - ? networkModel.NetworkTableType.topCountriesSource - : networkModel.NetworkTableType.topCountriesDestination; - } else { - tableType = - flowTargeted === FlowTargetSourceDest.source - ? networkModel.IpDetailsTableType.topCountriesSource - : networkModel.IpDetailsTableType.topCountriesDestination; + return flowTargeted === FlowTargetSourceDest.source + ? networkModel.NetworkTableType.topCountriesSource + : networkModel.NetworkTableType.topCountriesDestination; } - const field = - sort.field === NetworkTopTablesFields.bytes_out || - sort.field === NetworkTopTablesFields.bytes_in - ? `node.network.${sort.field}` - : `node.${flowTargeted}.${sort.field}`; - - const updateLimitPagination = useCallback( - (newLimit) => - updateNetworkTable({ + return flowTargeted === FlowTargetSourceDest.source + ? networkModel.IpDetailsTableType.topCountriesSource + : networkModel.IpDetailsTableType.topCountriesDestination; + }, [flowTargeted, type]); + + const field = + sort.field === NetworkTopTablesFields.bytes_out || + sort.field === NetworkTopTablesFields.bytes_in + ? `node.network.${sort.field}` + : `node.${flowTargeted}.${sort.field}`; + + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { limit: newLimit }, - }), - [type, updateNetworkTable, tableType] - ); - - const updateActivePage = useCallback( - (newPage) => - updateNetworkTable({ + }) + ), + [dispatch, type, tableType] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { activePage: newPage }, - }), - [type, updateNetworkTable, tableType] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const lastField = last(splitField); - const newSortDirection = - lastField !== sort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click - const newTopCountriesSort: NetworkTopTablesSortField = { - field: lastField as NetworkTopTablesFields, - direction: newSortDirection as Direction, - }; - if (!deepEqual(newTopCountriesSort, sort)) { - updateNetworkTable({ + }) + ), + [dispatch, type, tableType] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const lastField = last(splitField) as NetworkTopTablesFields; + const newSortDirection = + lastField !== sort.field ? Direction.desc : (criteria.sort.direction as Direction); // sort by desc on init click + const newTopCountriesSort: SortField = { + field: lastField, + direction: newSortDirection, + }; + if (!deepEqual(newTopCountriesSort, sort)) { + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { sort: newTopCountriesSort, }, - }); - } + }) + ); } - }, - [type, sort, tableType, updateNetworkTable] - ); - - const columns = useMemo( - () => - getCountriesColumnsCurated(indexPattern, flowTargeted, type, NetworkTopCountriesTableId), - [indexPattern, flowTargeted, type] - ); - - return ( - - ); - } -); - -NetworkTopCountriesTableComponent.displayName = 'NetworkTopCountriesTableComponent'; - -const makeMapStateToProps = () => { - const getTopCountriesSelector = networkSelectors.topCountriesSelector(); - return (state: State, { type, flowTargeted }: OwnProps) => - getTopCountriesSelector(state, type, flowTargeted); -}; - -const mapDispatchToProps = { - updateNetworkTable: networkActions.updateNetworkTable, + } + }, + [sort, dispatch, type, tableType] + ); + + const columns = useMemo( + () => getCountriesColumnsCurated(indexPattern, flowTargeted, type, NetworkTopCountriesTableId), + [indexPattern, flowTargeted, type] + ); + + return ( + + ); }; -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; +NetworkTopCountriesTableComponent.displayName = 'NetworkTopCountriesTableComponent'; -export const NetworkTopCountriesTable = connector(NetworkTopCountriesTableComponent); +export const NetworkTopCountriesTable = React.memo(NetworkTopCountriesTableComponent); diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/mock.ts b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/mock.ts index cee775c93d66fd..eb6843647f74ac 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/mock.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NetworkTopCountriesData } from '../../../graphql/types'; +import { NetworkTopCountriesStrategyResponse } from '../../../../common/search_strategy/security_solution/network'; -export const mockData: { NetworkTopCountries: NetworkTopCountriesData } = { +export const mockData: { NetworkTopCountries: NetworkTopCountriesStrategyResponse } = { NetworkTopCountries: { + rawResponse: {} as NetworkTopCountriesStrategyResponse['rawResponse'], totalCount: 524, edges: [ { diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index b167cba460818f..0b07991725f87c 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -4,161 +4,200 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; +import { noop } from 'lodash/fp'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; +import { ESTermQuery } from '../../../../common/typed_json'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { inputsModel, State } from '../../../common/store'; +import { useKibana } from '../../../common/lib/kibana'; +import { createFilter } from '../../../common/containers/helpers'; +import { PageInfoPaginated } from '../../../../common/search_strategy/security_solution'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { networkModel, networkSelectors } from '../../store'; import { FlowTargetSourceDest, - GetNetworkTopCountriesQuery, + NetworkQueries, NetworkTopCountriesEdges, - NetworkTopTablesSortField, - PageInfoPaginated, -} from '../../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../../common/store'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; -import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; -import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; -import { networkTopCountriesQuery } from './index.gql_query'; -import { networkModel, networkSelectors } from '../../store'; + NetworkTopCountriesRequestOptions, + NetworkTopCountriesStrategyResponse, +} from '../../../../common/search_strategy/security_solution/network'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import * as i18n from './translations'; const ID = 'networkTopCountriesQuery'; export interface NetworkTopCountriesArgs { id: string; - ip?: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; - loading: boolean; loadPage: (newActivePage: number) => void; - networkTopCountries: NetworkTopCountriesEdges[]; pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; + networkTopCountries: NetworkTopCountriesEdges[]; totalCount: number; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkTopCountriesArgs) => React.ReactNode; +interface UseNetworkTopCountries { flowTarget: FlowTargetSourceDest; ip?: string; type: networkModel.NetworkType; + filterQuery?: ESTermQuery | string; + endDate: string; + startDate: string; + skip: boolean; + id?: string; } -export interface NetworkTopCountriesComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkTopTablesSortField; -} +export const useNetworkTopCountries = ({ + endDate, + filterQuery, + flowTarget, + id = ID, + skip, + startDate, + type, +}: UseNetworkTopCountries): [boolean, NetworkTopCountriesArgs] => { + const getTopCountriesSelector = networkSelectors.topCountriesSelector(); + const { activePage, limit, sort } = useSelector( + (state: State) => getTopCountriesSelector(state, type, flowTarget), + shallowEqual + ); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + + const [networkTopCountriesRequest, setHostRequest] = useState({ + defaultIndex, + factoryQueryType: NetworkQueries.topCountries, + filterQuery: createFilter(filterQuery), + flowTarget, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + timerange: { + interval: '12h', + from: startDate ? startDate : '', + to: endDate ? endDate : new Date(Date.now()).toISOString(), + }, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setHostRequest((prevRequest) => ({ + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + })); + }, + [limit] + ); -type NetworkTopCountriesProps = OwnProps & NetworkTopCountriesComponentReduxProps & WithKibanaProps; + const [networkTopCountriesResponse, setNetworkTopCountriesResponse] = useState< + NetworkTopCountriesArgs + >({ + networkTopCountries: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + }); -class NetworkTopCountriesComponentQuery extends QueryTemplatePaginated< - NetworkTopCountriesProps, - GetNetworkTopCountriesQuery.Query, - GetNetworkTopCountriesQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - flowTarget, - filterQuery, - kibana, - id = `${ID}-${flowTarget}`, - ip, - isInspected, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetNetworkTopCountriesQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkTopCountriesQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkTopCountries = getOr([], `source.NetworkTopCountries.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + const networkTopCountriesSearch = useCallback( + (request: NetworkTopCountriesRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setNetworkTopCountriesResponse((prevResponse) => ({ + ...prevResponse, + networkTopCountries: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_NETWORK_TOP_COUNTRIES); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_NETWORK_TOP_COUNTRIES, + text: msg.message, + }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkTopCountries: { - ...fetchMoreResult.source.NetworkTopCountries, - edges: [...fetchMoreResult.source.NetworkTopCountries.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkTopCountries.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkTopCountries, - pageInfo: getOr({}, 'source.NetworkTopCountries.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkTopCountries.totalCount', data), }); - }} - - ); - } -} + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); -const makeMapStateToProps = () => { - const getTopCountriesSelector = networkSelectors.topCountriesSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTopCountriesSelector(state, type, flowTarget), - isInspected, - }; - }; -}; + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip]); -export const NetworkTopCountriesQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(NetworkTopCountriesComponentQuery); + useEffect(() => { + networkTopCountriesSearch(networkTopCountriesRequest); + }, [networkTopCountriesRequest, networkTopCountriesSearch]); + + return [loading, networkTopCountriesResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/translations.ts b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/translations.ts new file mode 100644 index 00000000000000..ff807ee268adf0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_NETWORK_TOP_COUNTRIES = i18n.translate( + 'xpack.securitySolution.networkTopCountries.errorSearchDescription', + { + defaultMessage: `An error has occurred on network top countries search`, + } +); + +export const FAIL_NETWORK_TOP_COUNTRIES = i18n.translate( + 'xpack.securitySolution.networkTopCountries.failSearchDescription', + { + defaultMessage: `Failed to run search on network top countries`, + } +); diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_countries_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_countries_query_table.tsx index 6bc80ef1a6aae4..42ddd3a6bb4a43 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_countries_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_countries_query_table.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkWithIndexComponentsQueryTableProps } from './types'; -import { NetworkTopCountriesQuery } from '../../containers/network_top_countries'; +import { useNetworkTopCountries } from '../../containers/network_top_countries'; import { NetworkTopCountriesTable } from '../../components/network_top_countries_table'; const NetworkTopCountriesTableManage = manageQuery(NetworkTopCountriesTable); @@ -23,46 +23,38 @@ export const NetworkTopCountriesQueryTable = ({ startDate, type, indexPattern, -}: NetworkWithIndexComponentsQueryTableProps) => ( - - {({ - id, - inspect, - isInspected, - loading, - loadPage, - networkTopCountries, - pageInfo, - refetch, - totalCount, - }) => ( - - )} - -); +}: NetworkWithIndexComponentsQueryTableProps) => { + const [ + loading, + { id, inspect, isInspected, loadPage, networkTopCountries, pageInfo, refetch, totalCount }, + ] = useNetworkTopCountries({ + endDate, + flowTarget, + filterQuery, + ip, + skip, + startDate, + type, + }); + + return ( + + ); +}; NetworkTopCountriesQueryTable.displayName = 'NetworkTopCountriesQueryTable'; diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts b/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts index 9691214cc2820c..d1ee48a9a5d9e7 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts @@ -8,7 +8,10 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { ESTermQuery } from '../../../../common/typed_json'; import { NetworkType } from '../../store/model'; -import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; +import { + FlowTarget, + FlowTargetSourceDest, +} from '../../../../common/search_strategy/security_solution/network'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; export const type = NetworkType.details; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx index 0c569952458e47..1e57ca42257e78 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { getOr } from 'lodash/fp'; import { NetworkTopCountriesTable } from '../../components/network_top_countries_table'; -import { NetworkTopCountriesQuery } from '../../containers/network_top_countries'; +import { useNetworkTopCountries } from '../../containers/network_top_countries'; import { networkModel } from '../../store'; import { manageQuery } from '../../../common/components/page/manage_query'; @@ -24,45 +24,37 @@ export const CountriesQueryTabBody = ({ setQuery, indexPattern, flowTarget, -}: CountriesQueryTabBodyProps) => ( - - {({ - id, - inspect, - isInspected, - loading, - loadPage, - networkTopCountries, - pageInfo, - refetch, - totalCount, - }) => ( - - )} - -); +}: CountriesQueryTabBodyProps) => { + const [ + loading, + { id, inspect, isInspected, loadPage, networkTopCountries, pageInfo, refetch, totalCount }, + ] = useNetworkTopCountries({ + endDate, + flowTarget, + filterQuery, + skip, + startDate, + type: networkModel.NetworkType.page, + }); + + return ( + + ); +}; CountriesQueryTabBody.displayName = 'CountriesQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx index 93582088811dca..2da56a30df7c75 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx @@ -8,7 +8,7 @@ import React, { useCallback } from 'react'; import { Route, Switch } from 'react-router-dom'; import { EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { FlowTargetSourceDest } from '../../../graphql/types'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy/security_solution/network'; import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; import { IPsQueryTabBody } from './ips_query_tab_body'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts index 183c760e40ab10..2ef04d3371c0b9 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts @@ -8,7 +8,7 @@ import { ESTermQuery } from '../../../../common/typed_json'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { NavTab } from '../../../common/components/navigation/types'; -import { FlowTargetSourceDest } from '../../../graphql/types'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy/security_solution/network'; import { networkModel } from '../../store'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; diff --git a/x-pack/plugins/security_solution/public/network/store/selectors.ts b/x-pack/plugins/security_solution/public/network/store/selectors.ts index cef8b139402eff..0246305092a323 100644 --- a/x-pack/plugins/security_solution/public/network/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/network/store/selectors.ts @@ -7,7 +7,7 @@ import { createSelector } from 'reselect'; import { get } from 'lodash/fp'; -import { FlowTargetSourceDest } from '../../graphql/types'; +import { FlowTargetSourceDest } from '../../../common/search_strategy/security_solution/network'; import { State } from '../../common/store/types'; import { initialNetworkState } from './reducer'; import { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/helpers.ts new file mode 100644 index 00000000000000..a7fba087b87edd --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/helpers.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { assertUnreachable } from '../../../../../common/utility_types'; +import { FlowTargetSourceDest } from '../../../../../common/search_strategy/security_solution/network'; + +export const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest => { + switch (flowTarget) { + case FlowTargetSourceDest.source: + return FlowTargetSourceDest.destination; + case FlowTargetSourceDest.destination: + return FlowTargetSourceDest.source; + } + assertUnreachable(flowTarget); +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts index 7d40b034c66bb0..93e5f113197da1 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts @@ -10,8 +10,10 @@ import { NetworkQueries } from '../../../../../common/search_strategy/security_s import { SecuritySolutionFactory } from '../types'; import { networkHttp } from './http'; import { networkTls } from './tls'; +import { networkTopCountries } from './top_countries'; export const networkFactory: Record> = { [NetworkQueries.http]: networkHttp, [NetworkQueries.tls]: networkTls, + [NetworkQueries.topCountries]: networkTopCountries, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/helpers.ts new file mode 100644 index 00000000000000..a8972c3d89f36c --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/helpers.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + NetworkTopCountriesBuckets, + NetworkTopCountriesEdges, + NetworkTopCountriesRequestOptions, + FlowTargetSourceDest, +} from '../../../../../../common/search_strategy/security_solution/network'; +import { getOppositeField } from '../helpers'; + +export const getTopCountriesEdges = ( + response: IEsSearchResponse, + options: NetworkTopCountriesRequestOptions +): NetworkTopCountriesEdges[] => + formatTopCountriesEdges( + getOr([], `aggregations.${options.flowTarget}.buckets`, response.rawResponse), + options.flowTarget + ); + +export const formatTopCountriesEdges = ( + buckets: NetworkTopCountriesBuckets[], + flowTarget: FlowTargetSourceDest +): NetworkTopCountriesEdges[] => + buckets.map((bucket: NetworkTopCountriesBuckets) => ({ + node: { + _id: bucket.key, + [flowTarget]: { + country: bucket.key, + flows: getOr(0, 'flows.value', bucket), + [`${getOppositeField(flowTarget)}_ips`]: getOr( + 0, + `${getOppositeField(flowTarget)}_ips.value`, + bucket + ), + [`${flowTarget}_ips`]: getOr(0, `${flowTarget}_ips.value`, bucket), + }, + network: { + bytes_in: getOr(0, 'bytes_in.value', bucket), + bytes_out: getOr(0, 'bytes_out.value', bucket), + }, + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.ts new file mode 100644 index 00000000000000..5b0ced06f2ee9f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + NetworkTopCountriesStrategyResponse, + NetworkQueries, + NetworkTopCountriesRequestOptions, + NetworkTopCountriesEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; + +import { getTopCountriesEdges } from './helpers'; +import { buildTopCountriesQuery } from './query.top_countries_network.dsl'; + +export const networkTopCountries: SecuritySolutionFactory = { + buildDsl: (options: NetworkTopCountriesRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildTopCountriesQuery(options); + }, + parse: async ( + options: NetworkTopCountriesRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.top_countries_count.value', response.rawResponse); + const networkTopCountriesEdges: NetworkTopCountriesEdges[] = getTopCountriesEdges( + response, + options + ); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = networkTopCountriesEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildTopCountriesQuery(options))], + response: [inspectStringifyObject(response)], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + ...response, + edges, + inspect, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + totalCount, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/query.top_countries_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/query.top_countries_network.dsl.ts new file mode 100644 index 00000000000000..88007b3329a908 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/query.top_countries_network.dsl.ts @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createQueryFilterClauses } from '../../../../../utils/build_query'; +import { assertUnreachable } from '../../../../../../common/utility_types'; +import { + Direction, + FlowTargetSourceDest, + NetworkTopTablesFields, + NetworkTopCountriesRequestOptions, + SortField, +} from '../../../../../../common/search_strategy/security_solution'; + +const getCountAgg = (flowTarget: FlowTargetSourceDest) => ({ + top_countries_count: { + cardinality: { + field: `${flowTarget}.geo.country_iso_code`, + }, + }, +}); + +export const buildTopCountriesQuery = ({ + defaultIndex, + filterQuery, + flowTarget, + sort, + pagination: { querySize }, + timerange: { from, to }, + ip, +}: NetworkTopCountriesRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + aggregations: { + ...getCountAgg(flowTarget), + ...getFlowTargetAggs(sort, flowTarget, querySize), + }, + query: { + bool: ip + ? { + filter, + should: [ + { + term: { + [`${getOppositeField(flowTarget)}.ip`]: ip, + }, + }, + ], + minimum_should_match: 1, + } + : { + filter, + }, + }, + }, + size: 0, + track_total_hits: false, + }; + return dslQuery; +}; + +const getFlowTargetAggs = ( + sort: SortField, + flowTarget: FlowTargetSourceDest, + querySize: number +) => ({ + [flowTarget]: { + terms: { + field: `${flowTarget}.geo.country_iso_code`, + size: querySize, + order: { + ...getQueryOrder(sort), + }, + }, + aggs: { + bytes_in: { + sum: { + field: `${getOppositeField(flowTarget)}.bytes`, + }, + }, + bytes_out: { + sum: { + field: `${flowTarget}.bytes`, + }, + }, + flows: { + cardinality: { + field: 'network.community_id', + }, + }, + source_ips: { + cardinality: { + field: 'source.ip', + }, + }, + destination_ips: { + cardinality: { + field: 'destination.ip', + }, + }, + }, + }, +}); + +export const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest => { + switch (flowTarget) { + case FlowTargetSourceDest.source: + return FlowTargetSourceDest.destination; + case FlowTargetSourceDest.destination: + return FlowTargetSourceDest.source; + } + assertUnreachable(flowTarget); +}; + +type QueryOrder = + | { bytes_in: Direction } + | { bytes_out: Direction } + | { flows: Direction } + | { destination_ips: Direction } + | { source_ips: Direction }; + +const getQueryOrder = ( + networkTopCountriesSortField: SortField +): QueryOrder => { + switch (networkTopCountriesSortField.field) { + case NetworkTopTablesFields.bytes_in: + return { bytes_in: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.bytes_out: + return { bytes_out: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.flows: + return { flows: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.destination_ips: + return { destination_ips: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.source_ips: + return { source_ips: networkTopCountriesSortField.direction }; + } + assertUnreachable(networkTopCountriesSortField.field); +};