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 3944dc53139e4e..4d6bd87bb9d420 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 @@ -21,6 +21,8 @@ import { } from './hosts'; import { NetworkQueries, + NetworkDnsStrategyResponse, + NetworkDnsRequestOptions, NetworkTlsStrategyResponse, NetworkTlsRequestOptions, NetworkHttpStrategyResponse, @@ -79,10 +81,12 @@ export type StrategyResponseType = T extends HostsQ ? HostFirstLastSeenStrategyResponse : T extends HostsQueries.uncommonProcesses ? HostUncommonProcessesStrategyResponse - : T extends NetworkQueries.tls - ? NetworkTlsStrategyResponse + : T extends NetworkQueries.dns + ? NetworkDnsStrategyResponse : T extends NetworkQueries.http ? NetworkHttpStrategyResponse + : T extends NetworkQueries.tls + ? NetworkTlsStrategyResponse : T extends NetworkQueries.topCountries ? NetworkTopCountriesStrategyResponse : T extends NetworkQueries.topNFlow @@ -101,10 +105,12 @@ export type StrategyRequestType = T extends HostsQu ? HostFirstLastSeenRequestOptions : T extends HostsQueries.uncommonProcesses ? HostUncommonProcessesRequestOptions - : T extends NetworkQueries.tls - ? NetworkTlsRequestOptions + : T extends NetworkQueries.dns + ? NetworkDnsRequestOptions : T extends NetworkQueries.http ? NetworkHttpRequestOptions + : T extends NetworkQueries.tls + ? NetworkTlsRequestOptions : T extends NetworkQueries.topCountries ? NetworkTopCountriesRequestOptions : T extends NetworkQueries.topNFlow diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns/index.ts new file mode 100644 index 00000000000000..e3899a914ee3a4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns/index.ts @@ -0,0 +1,65 @@ +/* + * 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 { CursorType, Inspect, Maybe, PageInfoPaginated, SortField } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; + +export enum NetworkDnsFields { + dnsName = 'dnsName', + queryCount = 'queryCount', + uniqueDomains = 'uniqueDomains', + dnsBytesIn = 'dnsBytesIn', + dnsBytesOut = 'dnsBytesOut', +} + +export interface NetworkDnsRequestOptions extends RequestOptionsPaginated { + isPtrIncluded: boolean; + sort: SortField; + stackByField?: Maybe; +} + +export interface NetworkDnsStrategyResponse extends IEsSearchResponse { + edges: NetworkDnsEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; + histogram?: Maybe; +} + +export interface NetworkDnsEdges { + node: NetworkDnsItem; + cursor: CursorType; +} + +export interface NetworkDnsItem { + _id?: Maybe; + dnsBytesIn?: Maybe; + dnsBytesOut?: Maybe; + dnsName?: Maybe; + queryCount?: Maybe; + uniqueDomains?: Maybe; +} + +export interface MatrixOverOrdinalHistogramData { + x: string; + y: number; + g: string; +} + +export interface NetworkDnsBuckets { + key: string; + doc_count: number; + unique_domains: { + value: number; + }; + dns_bytes_in: { + value: number; + }; + dns_bytes_out: { + value: number; + }; +} 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 2992ee32f8ac73..a0ef43eb3d8998 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 @@ -5,12 +5,14 @@ */ export * from './common'; +export * from './dns'; export * from './http'; export * from './tls'; export * from './top_countries'; export * from './top_n_flow'; export enum NetworkQueries { + dns = 'dns', http = 'http', tls = 'tls', topCountries = 'topCountries', 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 index f499db82d6479f..a28388a2c6f8f9 100644 --- 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 @@ -14,14 +14,6 @@ import { TopNetworkTablesEcsField, } from '../common'; -export enum NetworkDnsFields { - dnsName = 'dnsName', - queryCount = 'queryCount', - uniqueDomains = 'uniqueDomains', - dnsBytesIn = 'dnsBytesIn', - dnsBytesOut = 'dnsBytesOut', -} - export enum FlowTarget { client = 'client', destination = 'destination', diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/histogram.ts b/x-pack/plugins/security_solution/public/network/containers/network_dns/histogram.ts new file mode 100644 index 00000000000000..dce0c3bd2b30d4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/histogram.ts @@ -0,0 +1,65 @@ +/* + * 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 React from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { DocumentNode } from 'graphql'; +import { ScaleType } from '@elastic/charts'; + +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; +import { + MatrixHistogramOption, + GetSubTitle, +} from '../../../common/components/matrix_histogram/types'; +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { withKibana } from '../../../common/lib/kibana'; +import { QueryTemplatePaginatedProps } from '../../../common/containers/query_template_paginated'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../../common/store/constants'; +import { networkModel, networkSelectors } from '../../store'; +import { State, inputsSelectors } from '../../../common/store'; + +export const HISTOGRAM_ID = 'networkDnsHistogramQuery'; + +interface DnsHistogramOwnProps extends QueryTemplatePaginatedProps { + dataKey: string | string[]; + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + isDnsHistogram?: boolean; + query: DocumentNode; + scaleType: ScaleType; + setQuery: GlobalTimeArgs['setQuery']; + showLegend?: boolean; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string; + type: networkModel.NetworkType; + updateDateRange: UpdateDateRange; + yTickFormatter?: (value: number) => string; +} + +const makeMapHistogramStateToProps = () => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: DnsHistogramOwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getNetworkDnsSelector(state), + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + isInspected, + id, + }; + }; + + return mapStateToProps; +}; + +export const NetworkDnsHistogramQuery = compose>( + connect(makeMapHistogramStateToProps), + withKibana +)(MatrixHistogram); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 72e3161de53732..53d9a303ab849b 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -4,48 +4,38 @@ * 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 { DocumentNode } from 'graphql'; -import { ScaleType } from '@elastic/charts'; +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 { - GetNetworkDnsQuery, - NetworkDnsEdges, - NetworkDnsSortField, - PageInfoPaginated, - MatrixOverOrdinalHistogramData, -} from '../../../graphql/types'; -import { inputsModel, State, inputsSelectors } from '../../../common/store'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { inputsModel, State } from '../../../common/store'; +import { useKibana } from '../../../common/lib/kibana'; +import { createFilter } from '../../../common/containers/helpers'; +import { NetworkDnsEdges, PageInfoPaginated } from '../../../graphql/types'; 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 { networkDnsQuery } from './index.gql_query'; -import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../../common/store/constants'; -import { MatrixHistogram } from '../../../common/components/matrix_histogram'; -import { - MatrixHistogramOption, - GetSubTitle, -} from '../../../common/components/matrix_histogram/types'; -import { UpdateDateRange } from '../../../common/components/charts/common'; -import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { networkModel, networkSelectors } from '../../store'; +import { + NetworkQueries, + NetworkDnsRequestOptions, + NetworkDnsStrategyResponse, + MatrixOverOrdinalHistogramData, +} from '../../../../common/search_strategy/security_solution/network'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import * as i18n from './translations'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; + +export * from './histogram'; const ID = 'networkDnsQuery'; -export const HISTOGRAM_ID = 'networkDnsHistogramQuery'; + export interface NetworkDnsArgs { id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; - loading: boolean; loadPage: (newActivePage: number) => void; networkDns: NetworkDnsEdges[]; pageInfo: PageInfoPaginated; @@ -55,162 +45,164 @@ export interface NetworkDnsArgs { histogram: MatrixOverOrdinalHistogramData[]; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkDnsArgs) => React.ReactNode; +interface UseNetworkDns { + id?: string; type: networkModel.NetworkType; + filterQuery?: ESTermQuery | string; + endDate: string; + startDate: string; + skip: boolean; } -interface DnsHistogramOwnProps extends QueryTemplatePaginatedProps { - dataKey: string | string[]; - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - isDnsHistogram?: boolean; - query: DocumentNode; - scaleType: ScaleType; - setQuery: GlobalTimeArgs['setQuery']; - showLegend?: boolean; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title: string; - type: networkModel.NetworkType; - updateDateRange: UpdateDateRange; - yTickFormatter?: (value: number) => string; -} +export const useNetworkDns = ({ + endDate, + filterQuery, + id = ID, + skip, + startDate, + type, +}: UseNetworkDns): [boolean, NetworkDnsArgs] => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const { activePage, sort, isPtrIncluded, limit } = useSelector( + (state: State) => getNetworkDnsSelector(state), + 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); -export interface NetworkDnsComponentReduxProps { - activePage: number; - sort: NetworkDnsSortField; - isInspected: boolean; - isPtrIncluded: boolean; - limit: number; -} + const [networkDnsRequest, setNetworkDnsRequest] = useState({ + defaultIndex, + factoryQueryType: NetworkQueries.dns, + filterQuery: createFilter(filterQuery), + isPtrIncluded, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + timerange: { + interval: '12h', + from: startDate ? startDate : '', + to: endDate ? endDate : new Date(Date.now()).toISOString(), + }, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setNetworkDnsRequest((prevRequest) => ({ + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + })); + }, + [limit] + ); -type NetworkDnsProps = OwnProps & NetworkDnsComponentReduxProps & WithKibanaProps; - -export class NetworkDnsComponentQuery extends QueryTemplatePaginated< - NetworkDnsProps, - GetNetworkDnsQuery.Query, - GetNetworkDnsQuery.Variables -> { - public render() { - const { - activePage, - children, - sort, - endDate, - filterQuery, - id = ID, - isInspected, - isPtrIncluded, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetNetworkDnsQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - isPtrIncluded, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkDnsQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkDns = getOr([], `source.NetworkDns.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + const [networkDnsResponse, setNetworkDnsResponse] = useState({ + networkDns: [], + histogram: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + }); + + const networkDnsSearch = useCallback( + (request: NetworkDnsRequestOptions) => { + 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); + setNetworkDnsResponse((prevResponse) => ({ + ...prevResponse, + networkDns: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + histogram: response.histogram ?? prevResponse.histogram, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_NETWORK_DNS); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_NETWORK_DNS, + text: msg.message, + }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkDns: { - ...fetchMoreResult.source.NetworkDns, - edges: [...fetchMoreResult.source.NetworkDns.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkDns.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkDns, - pageInfo: getOr({}, 'source.NetworkDns.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkDns.totalCount', data), - histogram: getOr(null, 'source.NetworkDns.histogram', data), }); - }} - - ); - } -} + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); -const makeMapStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getNetworkDnsSelector(state), - isInspected, - id, - }; - }; - - return mapStateToProps; -}; + useEffect(() => { + if (skip) { + return; + } -const makeMapHistogramStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: DnsHistogramOwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getNetworkDnsSelector(state), - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - isInspected, - id, - }; - }; - - return mapStateToProps; -}; + setNetworkDnsRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + isPtrIncluded, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip, isPtrIncluded]); -export const NetworkDnsQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(NetworkDnsComponentQuery); + useEffect(() => { + networkDnsSearch(networkDnsRequest); + }, [networkDnsRequest, networkDnsSearch]); -export const NetworkDnsHistogramQuery = compose>( - connect(makeMapHistogramStateToProps), - withKibana -)(MatrixHistogram); + return [loading, networkDnsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/translations.ts b/x-pack/plugins/security_solution/public/network/containers/network_dns/translations.ts new file mode 100644 index 00000000000000..54c36dd1536f14 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/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_DNS = i18n.translate( + 'xpack.securitySolution.networkDns.errorSearchDescription', + { + defaultMessage: `An error has occurred on network dns search`, + } +); + +export const FAIL_NETWORK_DNS = i18n.translate( + 'xpack.securitySolution.networkDns.failSearchDescription', + { + defaultMessage: `Failed to run search on network dns`, + } +); 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 d1ee48a9a5d9e7..cab6e8e09b2003 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 @@ -11,7 +11,7 @@ import { NetworkType } from '../../store/model'; import { FlowTarget, FlowTargetSourceDest, -} from '../../../../common/search_strategy/security_solution/network'; +} from '../../../../common/search_strategy/security_solution'; 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/dns_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx index 051e85ab310c82..5adb78edbec8e5 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useCallback, useMemo } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkDnsTable } from '../../components/network_dns_table'; -import { NetworkDnsQuery, HISTOGRAM_ID } from '../../containers/network_dns'; +import { useNetworkDns, HISTOGRAM_ID } from '../../containers/network_dns'; import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkComponentQueryProps } from './types'; @@ -41,7 +41,7 @@ export const histogramConfigs: Omit = { subtitle: undefined, }; -export const DnsQueryTabBody = ({ +const DnsQueryTabBodyComponent: React.FC = ({ deleteQuery, endDate, filterQuery, @@ -49,7 +49,7 @@ export const DnsQueryTabBody = ({ startDate, setQuery, type, -}: NetworkComponentQueryProps) => { +}) => { useEffect(() => { return () => { if (deleteQuery) { @@ -58,6 +58,17 @@ export const DnsQueryTabBody = ({ }; }, [deleteQuery]); + const [ + loading, + { totalCount, networkDns, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useNetworkDns({ + endDate, + filterQuery, + skip, + startDate, + type, + }); + const getTitle = useCallback( (option: MatrixHistogramOption) => i18n.DOMAINS_COUNT_BY(option.text), [] @@ -82,43 +93,24 @@ export const DnsQueryTabBody = ({ startDate={startDate} {...dnsHistogramConfigs} /> - - {({ - totalCount, - loading, - networkDns, - pageInfo, - loadPage, - id, - inspect, - isInspected, - refetch, - }) => ( - - )} - + /> ); }; -DnsQueryTabBody.displayName = 'DNSQueryTabBody'; +DnsQueryTabBodyComponent.displayName = 'DnsQueryTabBodyComponent'; + +export const DnsQueryTabBody = React.memo(DnsQueryTabBodyComponent); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/helpers.ts new file mode 100644 index 00000000000000..aa242e6ece7bf2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/helpers.ts @@ -0,0 +1,40 @@ +/* + * 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 { get, getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + NetworkDnsBuckets, + NetworkDnsEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +export const getDnsEdges = (response: IEsSearchResponse): NetworkDnsEdges[] => + formatDnsEdges(getOr([], `aggregations.dns_name_query_count.buckets`, response.rawResponse)); + +export const formatDnsEdges = (buckets: NetworkDnsBuckets[]): NetworkDnsEdges[] => + buckets.map((bucket: NetworkDnsBuckets) => ({ + node: { + _id: bucket.key, + dnsBytesIn: getOrNumber('dns_bytes_in.value', bucket), + dnsBytesOut: getOrNumber('dns_bytes_out.value', bucket), + dnsName: bucket.key, + queryCount: bucket.doc_count, + uniqueDomains: getOrNumber('unique_domains.value', bucket), + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + })); + +const getOrNumber = (path: string, bucket: NetworkDnsBuckets) => { + const numb = get(path, bucket); + if (numb == null) { + return null; + } + return numb; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts new file mode 100644 index 00000000000000..8e734ca9d1179a --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts @@ -0,0 +1,58 @@ +/* + * 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 { + NetworkDnsStrategyResponse, + NetworkQueries, + NetworkDnsRequestOptions, + NetworkDnsEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; + +import { getDnsEdges } from './helpers'; +import { buildDnsQuery } from './query.dns_network.dsl'; + +export const networkDns: SecuritySolutionFactory = { + buildDsl: (options: NetworkDnsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildDnsQuery(options); + }, + parse: async ( + options: NetworkDnsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.dns_count.value', response.rawResponse); + const networkDnsEdges: NetworkDnsEdges[] = getDnsEdges(response); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = networkDnsEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildDnsQuery(options))], + }; + 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/dns/query.dns_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts new file mode 100644 index 00000000000000..85b9051189bfe9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts @@ -0,0 +1,134 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +import { assertUnreachable } from '../../../../../../common/utility_types'; +import { + Direction, + SortField, + NetworkDnsRequestOptions, + NetworkDnsFields, +} from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +type QueryOrder = + | { _count: Direction } + | { _key: Direction } + | { unique_domains: Direction } + | { dns_bytes_in: Direction } + | { dns_bytes_out: Direction }; + +const getQueryOrder = (sort: SortField): QueryOrder => { + switch (sort.field) { + case NetworkDnsFields.queryCount: + return { _count: sort.direction }; + case NetworkDnsFields.dnsName: + return { _key: sort.direction }; + case NetworkDnsFields.uniqueDomains: + return { unique_domains: sort.direction }; + case NetworkDnsFields.dnsBytesIn: + return { dns_bytes_in: sort.direction }; + case NetworkDnsFields.dnsBytesOut: + return { dns_bytes_out: sort.direction }; + } + assertUnreachable(sort.field); +}; + +const getCountAgg = () => ({ + dns_count: { + cardinality: { + field: 'dns.question.registered_domain', + }, + }, +}); + +const createIncludePTRFilter = (isPtrIncluded: boolean) => + isPtrIncluded + ? {} + : { + must_not: [ + { + term: { + 'dns.question.type': { + value: 'PTR', + }, + }, + }, + ], + }; + +export const buildDnsQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + isPtrIncluded, + sort, + pagination: { querySize }, + stackByField = 'dns.question.registered_domain', + timerange: { from, to }, +}: NetworkDnsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggregations: { + ...getCountAgg(), + dns_name_query_count: { + terms: { + field: stackByField, + size: querySize, + order: { + ...getQueryOrder(sort), + }, + }, + aggs: { + unique_domains: { + cardinality: { + field: 'dns.question.name', + }, + }, + dns_bytes_in: { + sum: { + field: 'source.bytes', + }, + }, + dns_bytes_out: { + sum: { + field: 'destination.bytes', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + ...createIncludePTRFilter(isPtrIncluded), + }, + }, + }, + size: 0, + track_total_hits: false, + }; + + return dslQuery; +}; 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 c5c98e5facbdfa..f2b4f851747973 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,12 +10,14 @@ import { } from '../../../../../common/search_strategy/security_solution'; import { SecuritySolutionFactory } from '../types'; +import { networkDns } from './dns'; import { networkHttp } from './http'; import { networkTls } from './tls'; import { networkTopCountries } from './top_countries'; import { networkTopNFlow } from './top_n_flow'; export const networkFactory: Record> = { + [NetworkQueries.dns]: networkDns, [NetworkQueries.http]: networkHttp, [NetworkQueries.tls]: networkTls, [NetworkQueries.topCountries]: networkTopCountries,