diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index f5d46078fcea..297e17fd127b 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './authentications'; export * from './all'; +export * from './authentications'; export * from './common'; -export * from './overview'; export * from './first_last_seen'; +export * from './overview'; export * from './uncommon_processes'; export enum HostsQueries { 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 7721f2ae97d7..3944dc53139e 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 @@ -30,6 +30,11 @@ import { NetworkTopNFlowStrategyResponse, NetworkTopNFlowRequestOptions, } from './network'; +import { + MatrixHistogramQuery, + MatrixHistogramRequestOptions, + MatrixHistogramStrategyResponse, +} from './matrix_histogram'; import { DocValueFields, TimerangeInput, @@ -39,9 +44,10 @@ import { } from '../common'; export * from './hosts'; +export * from './matrix_histogram'; export * from './network'; -export type FactoryQueryTypes = HostsQueries | NetworkQueries; +export type FactoryQueryTypes = HostsQueries | NetworkQueries | typeof MatrixHistogramQuery; export interface RequestBasicOptions extends IEsSearchRequest { timerange: TimerangeInput; @@ -81,6 +87,8 @@ export type StrategyResponseType = T extends HostsQ ? NetworkTopCountriesStrategyResponse : T extends NetworkQueries.topNFlow ? NetworkTopNFlowStrategyResponse + : T extends typeof MatrixHistogramQuery + ? MatrixHistogramStrategyResponse : never; export type StrategyRequestType = T extends HostsQueries.hosts @@ -101,4 +109,6 @@ export type StrategyRequestType = T extends HostsQu ? NetworkTopCountriesRequestOptions : T extends NetworkQueries.topNFlow ? NetworkTopNFlowRequestOptions + : T extends typeof MatrixHistogramQuery + ? MatrixHistogramRequestOptions : never; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/alerts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/alerts/index.ts new file mode 100644 index 000000000000..28953d7df855 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/alerts/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { HistogramBucket } from '../common'; + +export interface AlertsGroupData { + key: string; + doc_count: number; + alerts: { + buckets: HistogramBucket[]; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/anomalies/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/anomalies/index.ts new file mode 100644 index 000000000000..dbd7fe6d1c42 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/anomalies/index.ts @@ -0,0 +1,33 @@ +/* + * 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 { SearchHit } from '../../../common'; + +interface AnomaliesOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface AnomaliesActionGroupData { + key: number; + anomalies: { + bucket: AnomaliesOverTimeHistogramData[]; + }; + doc_count: number; +} + +export interface AnomalySource { + [field: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +export interface AnomalyHit extends SearchHit { + sort: string[]; + _source: AnomalySource; + aggregations: { + [agg: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/authentications/index.ts new file mode 100644 index 000000000000..23d656be5044 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/authentications/index.ts @@ -0,0 +1,19 @@ +/* + * 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 interface AuthenticationsOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface AuthenticationsActionGroupData { + key: number; + events: { + bucket: AuthenticationsOverTimeHistogramData[]; + }; + doc_count: number; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/common/index.ts new file mode 100644 index 000000000000..687d55414f78 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/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 interface HistogramBucket { + key: number; + doc_count: number; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/dns/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/dns/index.ts new file mode 100644 index 000000000000..7667dce383e5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/dns/index.ts @@ -0,0 +1,25 @@ +/* + * 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 interface DnsHistogramSubBucket { + key: string; + doc_count: number; + orderAgg: { + value: number; + }; +} +interface DnsHistogramBucket { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: DnsHistogramSubBucket[]; +} + +export interface DnsHistogramGroupData { + key: number; + doc_count: number; + key_as_string: string; + histogram: DnsHistogramBucket; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts new file mode 100644 index 000000000000..f1307335215e --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { SearchHit } from '../../../common'; + +interface EventsMatrixHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface EventSource { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [field: string]: any; +} + +export interface EventsActionGroupData { + key: number; + events: { + bucket: EventsMatrixHistogramData[]; + }; + doc_count: number; +} + +export interface EventHit extends SearchHit { + sort: string[]; + _source: EventSource; + aggregations: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [agg: string]: any; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts new file mode 100644 index 000000000000..238300801cfc --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts @@ -0,0 +1,92 @@ +/* + * 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 { AuthenticationHit } from '../hosts'; +import { Inspect, Maybe, TimerangeInput } from '../../common'; +import { RequestBasicOptions } from '../'; +import { AlertsGroupData } from './alerts'; +import { AnomaliesActionGroupData, AnomalyHit } from './anomalies'; +import { DnsHistogramGroupData } from './dns'; +import { AuthenticationsActionGroupData } from './authentications'; +import { EventsActionGroupData, EventHit } from './events'; + +export * from './alerts'; +export * from './anomalies'; +export * from './authentications'; +export * from './common'; +export * from './dns'; +export * from './events'; + +export const MatrixHistogramQuery = 'matrixHistogram'; + +export enum MatrixHistogramType { + authentications = 'authentications', + anomalies = 'anomalies', + events = 'events', + alerts = 'alerts', + dns = 'dns', +} + +export interface MatrixHistogramRequestOptions extends RequestBasicOptions { + timerange: TimerangeInput; + histogramType: MatrixHistogramType; + stackByField: string; + inspect?: Maybe; +} + +export interface MatrixHistogramStrategyResponse extends IEsSearchResponse { + inspect?: Maybe; + matrixHistogramData: MatrixHistogramData[]; + totalCount: number; +} + +export interface MatrixHistogramData { + x?: Maybe; + y?: Maybe; + g?: Maybe; +} + +export interface MatrixHistogramBucket { + key: number; + doc_count: number; +} + +export interface MatrixHistogramSchema { + buildDsl: (options: MatrixHistogramRequestOptions) => {}; + aggName: string; + parseKey: string; + parser?: (data: MatrixHistogramParseData, keyBucket: string) => MatrixHistogramData[]; +} + +export type MatrixHistogramParseData = T extends MatrixHistogramType.alerts + ? AlertsGroupData[] + : T extends MatrixHistogramType.anomalies + ? AnomaliesActionGroupData[] + : T extends MatrixHistogramType.dns + ? DnsHistogramGroupData[] + : T extends MatrixHistogramType.authentications + ? AuthenticationsActionGroupData[] + : T extends MatrixHistogramType.events + ? EventsActionGroupData[] + : never; + +export type MatrixHistogramHit = T extends MatrixHistogramType.alerts + ? EventHit + : T extends MatrixHistogramType.anomalies + ? AnomalyHit + : T extends MatrixHistogramType.dns + ? EventHit + : T extends MatrixHistogramType.authentications + ? AuthenticationHit + : T extends MatrixHistogramType.events + ? EventHit + : never; + +export type MatrixHistogramDataConfig = Record< + MatrixHistogramType, + MatrixHistogramSchema +>; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/histogram_configs.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/histogram_configs.ts index c7376b67c518..ce79d839f216 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/histogram_configs.ts +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/histogram_configs.ts @@ -6,7 +6,7 @@ import * as i18n from './translations'; import { MatrixHistogramOption, MatrixHistogramConfigs } from '../matrix_histogram/types'; -import { HistogramType } from '../../../graphql/types'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution/matrix_histogram'; export const alertsStackByOptions: MatrixHistogramOption[] = [ { @@ -25,7 +25,7 @@ export const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: alertsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[1], errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA, - histogramType: HistogramType.alerts, + histogramType: MatrixHistogramType.alerts, stackByOptions: alertsStackByOptions, subtitle: undefined, title: i18n.ALERTS_GRAPH_TITLE, diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index de9a8b32f1f9..d522e372d773 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -13,12 +13,13 @@ import { AlertsComponentsProps } from './types'; import { AlertsTable } from './alerts_table'; import * as i18n from './translations'; import { useUiSetting$ } from '../../lib/kibana'; -import { MatrixHistogramContainer } from '../matrix_histogram'; +import { MatrixHistogram } from '../matrix_histogram'; import { histogramConfigs } from './histogram_configs'; import { MatrixHistogramConfigs } from '../matrix_histogram/types'; -const ID = 'alertsOverTimeQuery'; -export const AlertsView = ({ +const ID = 'alertsHistogramQuery'; + +const AlertsViewComponent: React.FC = ({ timelineId, deleteQuery, endDate, @@ -26,18 +27,18 @@ export const AlertsView = ({ pageFilters, setQuery, startDate, - type, -}: AlertsComponentsProps) => { +}) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const { globalFullScreen } = useFullScreen(); + const getSubtitle = useCallback( (totalCount: number) => `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${i18n.UNIT( totalCount )}`, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [defaultNumberFormat] ); - const { globalFullScreen } = useFullScreen(); + const alertsHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, @@ -45,6 +46,7 @@ export const AlertsView = ({ }), [getSubtitle] ); + useEffect(() => { return () => { if (deleteQuery) { @@ -56,14 +58,12 @@ export const AlertsView = ({ return ( <> {!globalFullScreen && ( - )} @@ -76,4 +76,7 @@ export const AlertsView = ({ ); }; -AlertsView.displayName = 'AlertsView'; + +AlertsViewComponent.displayName = 'AlertsViewComponent'; + +export const AlertsView = React.memo(AlertsViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts index 78a6332c90fb..b2637eeb2c65 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts @@ -15,7 +15,7 @@ type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; export interface AlertsComponentsProps extends Pick< CommonQueryProps, - 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' | 'type' + 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' > { timelineId: TimelineIdLiteral; pageFilters: Filter[]; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx index a80ea48b93f3..7286c6b74369 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx @@ -10,42 +10,32 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; import { MatrixHistogram } from '.'; -import { useQuery } from '../../containers/matrix_histogram'; -import { HistogramType } from '../../../graphql/types'; +import { useMatrixHistogram } from '../../containers/matrix_histogram'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; +import { TestProviders } from '../../mock'; + jest.mock('../../lib/kibana'); -jest.mock('./matrix_loader', () => { - return { - MatrixLoader: () => { - return
; - }, - }; -}); +jest.mock('./matrix_loader', () => ({ + MatrixLoader: () =>
, +})); -jest.mock('../header_section', () => { - return { - HeaderSection: () =>
, - }; -}); +jest.mock('../header_section', () => ({ + HeaderSection: () =>
, +})); -jest.mock('../charts/barchart', () => { - return { - BarChart: () =>
, - }; -}); +jest.mock('../charts/barchart', () => ({ + BarChart: () =>
, +})); -jest.mock('../../containers/matrix_histogram', () => { - return { - useQuery: jest.fn(), - }; -}); +jest.mock('../../containers/matrix_histogram', () => ({ + useMatrixHistogram: jest.fn(), +})); -jest.mock('../../components/matrix_histogram/utils', () => { - return { - getBarchartConfigs: jest.fn(), - getCustomChartData: jest.fn().mockReturnValue(true), - }; -}); +jest.mock('../../components/matrix_histogram/utils', () => ({ + getBarchartConfigs: jest.fn(), + getCustomChartData: jest.fn().mockReturnValue(true), +})); describe('Matrix Histogram Component', () => { let wrapper: ReactWrapper; @@ -55,7 +45,7 @@ describe('Matrix Histogram Component', () => { defaultStackByOption: { text: 'text', value: 'value' }, endDate: '2019-07-18T20:00:00.000Z', errorMessage: 'error', - histogramType: HistogramType.alerts, + histogramType: MatrixHistogramType.alerts, id: 'mockId', isInspected: false, isPtrIncluded: false, @@ -68,17 +58,20 @@ describe('Matrix Histogram Component', () => { subtitle: 'mockSubtitle', totalCount: -1, title: 'mockTitle', - dispatchSetAbsoluteRangeDatePicker: jest.fn(), }; beforeAll(() => { - (useQuery as jest.Mock).mockReturnValue({ - data: null, - loading: false, - inspect: false, - totalCount: null, + (useMatrixHistogram as jest.Mock).mockReturnValue([ + false, + { + data: null, + inspect: false, + totalCount: null, + }, + ]); + wrapper = mount(, { + wrappingComponent: TestProviders, }); - wrapper = mount(); }); describe('on initial load', () => { test('it renders MatrixLoader', () => { @@ -92,26 +85,33 @@ describe('Matrix Histogram Component', () => { }); test('it does NOT render a spacer when showSpacer is false', () => { - wrapper = mount(); + wrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(false); }); }); describe('not initial load', () => { beforeAll(() => { - (useQuery as jest.Mock).mockReturnValue({ - data: [ - { x: 1, y: 2, g: 'g1' }, - { x: 2, y: 4, g: 'g1' }, - { x: 3, y: 6, g: 'g1' }, - { x: 1, y: 1, g: 'g2' }, - { x: 2, y: 3, g: 'g2' }, - { x: 3, y: 5, g: 'g2' }, - ], - loading: false, - inspect: false, - totalCount: 1, - }); + (useMatrixHistogram as jest.Mock).mockReturnValue([ + false, + { + data: [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + { x: 1, y: 1, g: 'g2' }, + { x: 2, y: 3, g: 'g2' }, + { x: 3, y: 5, g: 'g2' }, + ], + inspect: false, + totalCount: 1, + }, + ]); wrapper.setProps({ endDate: 100 }); wrapper.update(); }); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index e93ade7191f5..485ca4c93133 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -9,54 +9,46 @@ import { Position } from '@elastic/charts'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import { compose } from 'redux'; -import { connect } from 'react-redux'; +import { useDispatch } from 'react-redux'; import * as i18n from './translations'; import { BarChart } from '../charts/barchart'; import { HeaderSection } from '../header_section'; import { MatrixLoader } from './matrix_loader'; import { Panel } from '../panel'; import { getBarchartConfigs, getCustomChartData } from './utils'; -import { useQuery } from '../../containers/matrix_histogram'; +import { useMatrixHistogram } from '../../containers/matrix_histogram'; import { MatrixHistogramProps, MatrixHistogramOption, MatrixHistogramQueryProps } from './types'; import { InspectButtonContainer } from '../inspect'; - -import { State, inputsSelectors } from '../../store'; -import { hostsModel } from '../../../hosts/store'; -import { networkModel } from '../../../network/store'; - +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { MatrixHistogramMappingTypes, GetTitle, GetSubTitle, } from '../../components/matrix_histogram/types'; import { GlobalTimeArgs } from '../../containers/use_global_time'; -import { QueryTemplateProps } from '../../containers/query_template'; import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { InputsModelId } from '../../store/inputs/constants'; -import { HistogramType } from '../../../graphql/types'; -export interface OwnProps extends QueryTemplateProps { - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - headerChildren?: React.ReactNode; - hideHistogramIfEmpty?: boolean; - histogramType: HistogramType; - id: string; - indexToAdd?: string[] | null; - legendPosition?: Position; - mapping?: MatrixHistogramMappingTypes; - showSpacer?: boolean; - setQuery: GlobalTimeArgs['setQuery']; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - showLegend?: boolean; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - timelineId?: string; - title: string | GetTitle; - type: hostsModel.HostsType | networkModel.NetworkType; -} +export type MatrixHistogramComponentProps = MatrixHistogramProps & + Omit & { + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + headerChildren?: React.ReactNode; + hideHistogramIfEmpty?: boolean; + histogramType: MatrixHistogramType; + id: string; + indexToAdd?: string[] | null; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + showSpacer?: boolean; + setQuery: GlobalTimeArgs['setQuery']; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + showLegend?: boolean; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + timelineId?: string; + title: string | GetTitle; + }; const DEFAULT_PANEL_HEIGHT = 300; @@ -70,9 +62,7 @@ const HistogramPanel = styled(Panel)<{ height?: number }>` ${({ height }) => (height != null ? `height: ${height}px;` : '')} `; -export const MatrixHistogramComponent: React.FC< - MatrixHistogramProps & MatrixHistogramQueryProps -> = ({ +export const MatrixHistogramComponent: React.FC = ({ chartHeight, defaultStackByOption, endDate, @@ -83,7 +73,6 @@ export const MatrixHistogramComponent: React.FC< hideHistogramIfEmpty = false, id, indexToAdd, - isInspected, legendPosition, mapping, panelHeight = DEFAULT_PANEL_HEIGHT, @@ -97,9 +86,25 @@ export const MatrixHistogramComponent: React.FC< timelineId, title, titleSize, - dispatchSetAbsoluteRangeDatePicker, yTickFormatter, }) => { + const dispatch = useDispatch(); + const handleBrushEnd = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + dispatch( + setAbsoluteRangeDatePicker({ + id: setAbsoluteRangeDatePickerTarget, + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }) + ); + }, + [dispatch, setAbsoluteRangeDatePickerTarget] + ); const barchartConfigs = useMemo( () => getBarchartConfigs({ @@ -107,30 +112,11 @@ export const MatrixHistogramComponent: React.FC< from: startDate, legendPosition, to: endDate, - onBrushEnd: ({ x }) => { - if (!x) { - return; - } - const [min, max] = x; - dispatchSetAbsoluteRangeDatePicker({ - id: setAbsoluteRangeDatePickerTarget, - from: new Date(min).toISOString(), - to: new Date(max).toISOString(), - }); - }, + onBrushEnd: handleBrushEnd, yTickFormatter, showLegend, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - chartHeight, - startDate, - legendPosition, - endDate, - dispatchSetAbsoluteRangeDatePicker, - yTickFormatter, - showLegend, - ] + [chartHeight, startDate, legendPosition, endDate, handleBrushEnd, yTickFormatter, showLegend] ); const [isInitialLoading, setIsInitialLoading] = useState(true); const [selectedStackByOption, setSelectedStackByOption] = useState( @@ -142,18 +128,16 @@ export const MatrixHistogramComponent: React.FC< stackByOptions.find((co) => co.value === event.target.value) ?? defaultStackByOption ); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [defaultStackByOption, stackByOptions] ); - const { data, loading, inspect, totalCount, refetch = noop } = useQuery({ + const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogram({ endDate, errorMessage, filterQuery, histogramType, indexToAdd, startDate, - isInspected, stackByField: selectedStackByOption.value, }); @@ -254,20 +238,3 @@ export const MatrixHistogramComponent: React.FC< }; export const MatrixHistogram = React.memo(MatrixHistogramComponent); - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -export const MatrixHistogramContainer = compose>( - connect(makeMapStateToProps, { - dispatchSetAbsoluteRangeDatePicker: setAbsoluteRangeDatePicker, - }) -)(MatrixHistogram); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index d471b5ae9bed..fc1df4d8ca85 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -9,7 +9,7 @@ import { ScaleType, Position, TickFormatter } from '@elastic/charts'; import { ActionCreator } from 'redux'; import { ESQuery } from '../../../../common/typed_json'; import { InputsModelId } from '../../store/inputs/constants'; -import { HistogramType } from '../../../graphql/types'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { UpdateDateRange } from '../charts/common'; import { GlobalTimeArgs } from '../../containers/use_global_time'; @@ -29,7 +29,7 @@ export interface MatrixHistogramConfigs { defaultStackByOption: MatrixHistogramOption; errorMessage: string; hideHistogramIfEmpty?: boolean; - histogramType: HistogramType; + histogramType: MatrixHistogramType; legendPosition?: Position; mapping?: MatrixHistogramMappingTypes; stackByOptions: MatrixHistogramOption[]; @@ -40,13 +40,7 @@ export interface MatrixHistogramConfigs { interface MatrixHistogramBasicProps { chartHeight?: number; - defaultIndex: string[]; defaultStackByOption: MatrixHistogramOption; - dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ - id: InputsModelId; - from: string; - to: string; - }>; endDate: GlobalTimeArgs['to']; headerChildren?: React.ReactNode; hideHistogramIfEmpty?: boolean; @@ -75,8 +69,7 @@ export interface MatrixHistogramQueryProps { stackByField: string; startDate: string; indexToAdd?: string[] | null; - isInspected: boolean; - histogramType: HistogramType; + histogramType: MatrixHistogramType; } export interface MatrixHistogramProps extends MatrixHistogramBasicProps { diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts index 6a05f97da2fe..b4893fa37571 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts @@ -8,7 +8,7 @@ import { MatrixHistogramOption, MatrixHistogramConfigs, } from '../../../components/matrix_histogram/types'; -import { HistogramType } from '../../../../graphql/types'; +import { MatrixHistogramType } from '../../../../../common/search_strategy/security_solution/matrix_histogram'; export const anomaliesStackByOptions: MatrixHistogramOption[] = [ { @@ -24,7 +24,7 @@ export const histogramConfigs: MatrixHistogramConfigs = { anomaliesStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? anomaliesStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_ANOMALIES_DATA, hideHistogramIfEmpty: true, - histogramType: HistogramType.anomalies, + histogramType: MatrixHistogramType.anomalies, stackByOptions: anomaliesStackByOptions, subtitle: undefined, title: i18n.ANOMALIES_TITLE, diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx index 94019b26c180..f6ebbb990f22 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -11,11 +11,12 @@ import { AnomaliesQueryTabBodyProps } from './types'; import { getAnomaliesFilterQuery } from './utils'; import { useInstalledSecurityJobs } from '../../../components/ml/hooks/use_installed_security_jobs'; import { useUiSetting$ } from '../../../lib/kibana'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { MatrixHistogram } from '../../../components/matrix_histogram'; import { histogramConfigs } from './histogram_configs'; -const ID = 'anomaliesOverTimeQuery'; -export const AnomaliesQueryTabBody = ({ +const ID = 'anomaliesHistogramQuery'; + +const AnomaliesQueryTabBodyComponent: React.FC = ({ deleteQuery, endDate, setQuery, @@ -28,16 +29,7 @@ export const AnomaliesQueryTabBody = ({ AnomaliesTableComponent, flowTarget, ip, -}: AnomaliesQueryTabBodyProps) => { - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - +}) => { const { jobs } = useInstalledSecurityJobs(); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); @@ -50,16 +42,23 @@ export const AnomaliesQueryTabBody = ({ ip ); + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( <> - ({ - useApolloClient: jest.fn(), -})); - -jest.mock('../../lib/kibana', () => { - return { - useUiSetting$: jest.fn().mockReturnValue(['mockDefaultIndex']), - }; -}); - -jest.mock('./index.gql_query', () => { - return { - MatrixHistogramGqlQuery: 'mockGqlQuery', - }; -}); - -jest.mock('../../components/toasters/', () => ({ - useStateToaster: () => [jest.fn(), jest.fn()], - errorToToaster: jest.fn(), -})); - -describe('useQuery', () => { - let result: { - data: MatrixOverTimeHistogramData[] | null; - loading: boolean; - inspect: InspectQuery | null; - totalCount: number; - refetch: Refetch | undefined; - }; - describe('happy path', () => { - beforeAll(() => { - (useApolloClient as jest.Mock).mockReturnValue({ - query: mockQuery, - }); - const TestComponent = () => { - result = useQuery({ - endDate: '2020-07-07T08:20:00.000Z', - errorMessage: 'fakeErrorMsg', - filterQuery: '', - histogramType: HistogramType.alerts, - isInspected: false, - stackByField: 'fakeField', - startDate: '2020-07-07T08:08:00.000Z', - }); - - return
; - }; - - mount(); - }); - - test('should set variables', () => { - expect(mockQuery).toBeCalledWith({ - query: 'mockGqlQuery', - fetchPolicy: 'network-only', - variables: { - filterQuery: '', - sourceId: 'default', - timerange: { - interval: '12h', - from: '2020-07-07T08:08:00.000Z', - to: '2020-07-07T08:20:00.000Z', - }, - defaultIndex: 'mockDefaultIndex', - inspect: false, - stackByField: 'fakeField', - histogramType: 'alerts', - }, - context: { - fetchOptions: { - abortSignal: new AbortController().signal, - }, - }, - }); - }); - - test('should setData', () => { - expect(result.data).toEqual([{}]); - }); - - test('should set total count', () => { - expect(result.totalCount).toEqual(1); - }); - - test('should set inspect', () => { - expect(result.inspect).toEqual(false); - }); - }); - - describe('failure path', () => { - beforeAll(() => { - mockQuery.mockClear(); - (useApolloClient as jest.Mock).mockReset(); - (useApolloClient as jest.Mock).mockReturnValue({ - query: mockRejectQuery, - }); - const TestComponent = () => { - result = useQuery({ - endDate: '2020-07-07T08:20:18.966Z', - errorMessage: 'fakeErrorMsg', - filterQuery: '', - histogramType: HistogramType.alerts, - isInspected: false, - stackByField: 'fakeField', - startDate: '2020-07-08T08:20:18.966Z', - }); - - return
; - }; - - mount(); - }); - - test('should setData', () => { - expect(result.data).toEqual(null); - }); - - test('should set total count', () => { - expect(result.totalCount).toEqual(-1); - }); - - test('should set inspect', () => { - expect(result.inspect).toEqual(null); - }); - - test('should set error to toster', () => { - expect(errorToToaster).toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index c4702e915c07..65ad3cc994c6 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -5,29 +5,44 @@ */ import deepEqual from 'fast-deep-equal'; -import { isEmpty } from 'lodash/fp'; -import { useEffect, useMemo, useState, useRef } from 'react'; +import { isEmpty, noop } from 'lodash/fp'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; -import { useUiSetting$ } from '../../lib/kibana'; -import { createFilter } from '../helpers'; -import { useApolloClient } from '../../utils/apollo_context'; -import { inputsModel } from '../../store'; -import { MatrixHistogramGqlQuery } from './index.gql_query'; -import { GetMatrixHistogramQuery, MatrixOverTimeHistogramData } from '../../../graphql/types'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { inputsModel } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; +import { + MatrixHistogramQuery, + MatrixHistogramRequestOptions, + MatrixHistogramStrategyResponse, + MatrixHistogramData, +} from '../../../../common/search_strategy/security_solution'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import * as i18n from './translations'; -export const useQuery = ({ +export interface UseMatrixHistogramArgs { + data: MatrixHistogramData[]; + inspect: InspectResponse; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export const useMatrixHistogram = ({ endDate, errorMessage, filterQuery, histogramType, indexToAdd, - isInspected, stackByField, startDate, -}: MatrixHistogramQueryProps) => { +}: MatrixHistogramQueryProps): [boolean, UseMatrixHistogramArgs] => { + const { data, notifications } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const defaultIndex = useMemo(() => { if (indexToAdd != null && !isEmpty(indexToAdd)) { @@ -35,108 +50,110 @@ export const useQuery = ({ } return configIndex; }, [configIndex, indexToAdd]); - const [, dispatchToaster] = useStateToaster(); - const refetch = useRef(); - const [loading, setLoading] = useState(false); - const [data, setData] = useState(null); - const [inspect, setInspect] = useState(null); - const [totalCount, setTotalCount] = useState(-1); - const apolloClient = useApolloClient(); - - const [matrixHistogramVariables, setMatrixHistogramVariables] = useState< - GetMatrixHistogramQuery.Variables + const [loading, setLoading] = useState(false); + const [matrixHistogramRequest, setMatrixHistogramRequest] = useState< + MatrixHistogramRequestOptions >({ + defaultIndex, + factoryQueryType: MatrixHistogramQuery, filterQuery: createFilter(filterQuery), - sourceId: 'default', + histogramType, timerange: { interval: '12h', - from: startDate!, - to: endDate!, + from: startDate, + to: endDate, }, - defaultIndex, - inspect: isInspected, stackByField, - histogramType, }); + const [matrixHistogramResponse, setMatrixHistogramResponse] = useState({ + data: [], + inspect: { + dsl: [], + response: [], + }, + refetch: refetch.current, + totalCount: -1, + }); + + const hostsSearch = useCallback( + (request: MatrixHistogramRequestOptions) => { + 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); + setMatrixHistogramResponse((prevResponse) => ({ + ...prevResponse, + data: response.matrixHistogramData, + inspect: getInspectResponse(response, prevResponse.inspect), + 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_MATRIX_HISTOGRAM); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: errorMessage ?? i18n.FAIL_MATRIX_HISTOGRAM, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, errorMessage, notifications.toasts] + ); + useEffect(() => { - setMatrixHistogramVariables((prevVariables) => { - const localVariables = { + setMatrixHistogramRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, filterQuery: createFilter(filterQuery), - sourceId: 'default', timerange: { interval: '12h', - from: startDate!, - to: endDate!, + from: startDate, + to: endDate, }, - defaultIndex, - inspect: isInspected, - stackByField, - histogramType, }; - if (!deepEqual(prevVariables, localVariables)) { - return localVariables; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; } - return prevVariables; + return prevRequest; }); - }, [ - defaultIndex, - filterQuery, - histogramType, - indexToAdd, - isInspected, - stackByField, - startDate, - endDate, - ]); + }, [defaultIndex, endDate, filterQuery, startDate]); useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - const abortSignal = abortCtrl.signal; - - async function fetchData() { - if (!apolloClient) return null; - setLoading(true); - return apolloClient - .query({ - query: MatrixHistogramGqlQuery, - fetchPolicy: 'network-only', - variables: matrixHistogramVariables, - context: { - fetchOptions: { - abortSignal, - }, - }, - }) - .then( - (result) => { - if (isSubscribed) { - const source = result?.data?.source?.MatrixHistogram ?? {}; - setData(source?.matrixHistogramData ?? []); - setTotalCount(source?.totalCount ?? -1); - setInspect(source?.inspect ?? null); - setLoading(false); - } - }, - (error) => { - if (isSubscribed) { - setData(null); - setTotalCount(-1); - setInspect(null); - setLoading(false); - errorToToaster({ title: errorMessage, error, dispatchToaster }); - } - } - ); - } - refetch.current = fetchData; - fetchData(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [apolloClient, dispatchToaster, errorMessage, matrixHistogramVariables]); + hostsSearch(matrixHistogramRequest); + }, [matrixHistogramRequest, hostsSearch]); - return { data, loading, inspect, totalCount, refetch: refetch.current }; + return [loading, matrixHistogramResponse]; }; diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/translations.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/translations.ts new file mode 100644 index 000000000000..b15b28c6b49a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/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_MATRIX_HISTOGRAM = i18n.translate( + 'xpack.securitySolution.matrixHistogram.errorSearchDescription', + { + defaultMessage: `An error has occurred on matrix histogram search`, + } +); + +export const FAIL_MATRIX_HISTOGRAM = i18n.translate( + 'xpack.securitySolution.matrixHistogram.failSearchDescription', + { + defaultMessage: `Failed to run search on matrix histogram`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index 543646940919..34f2385051f4 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -54,6 +54,7 @@ interface UseAuthentications { endDate: string; startDate: string; type: hostsModel.HostsType; + skip: boolean; } export const useAuthentications = ({ @@ -62,6 +63,7 @@ export const useAuthentications = ({ endDate, startDate, type, + skip, }: UseAuthentications): [boolean, AuthenticationArgs] => { const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); const { activePage, limit } = useSelector( @@ -190,12 +192,12 @@ export const useAuthentications = ({ to: endDate, }, }; - if (!deepEqual(prevRequest, myRequest)) { + if (!skip && !deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, startDate]); + }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, skip, startDate]); useEffect(() => { authenticationsSearch(authenticationsRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 084d4b699e8e..65ddb9305f60 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -10,19 +10,20 @@ import { AuthenticationTable } from '../../components/authentications_table'; import { manageQuery } from '../../../common/components/page/manage_query'; import { useAuthentications } from '../../containers/authentications'; import { HostsComponentsQueryProps } from './types'; -import { hostsModel } from '../../store'; import { MatrixHistogramOption, MatrixHistogramMappingTypes, MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; import { KpiHostsChartColors } from '../../components/kpi_hosts/types'; import * as i18n from '../translations'; -import { HistogramType } from '../../../graphql/types'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; const AuthenticationTableManage = manageQuery(AuthenticationTable); -const ID = 'authenticationsOverTimeQuery'; + +const ID = 'authenticationsHistogramQuery'; + const authStackByOptions: MatrixHistogramOption[] = [ { text: 'event.outcome', @@ -53,13 +54,13 @@ const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: authStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? authStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA, - histogramType: HistogramType.authentications, + histogramType: MatrixHistogramType.authentications, mapping: authMatrixDataMappingFields, stackByOptions: authStackByOptions, title: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, }; -export const AuthenticationsQueryTabBody = ({ +const AuthenticationsQueryTabBodyComponent: React.FC = ({ deleteQuery, docValueFields, endDate, @@ -68,7 +69,12 @@ export const AuthenticationsQueryTabBody = ({ setQuery, startDate, type, -}: HostsComponentsQueryProps) => { +}) => { + const [ + loading, + { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useAuthentications({ docValueFields, endDate, filterQuery, skip, startDate, type }); + useEffect(() => { return () => { if (deleteQuery) { @@ -77,21 +83,14 @@ export const AuthenticationsQueryTabBody = ({ }; }, [deleteQuery]); - const [ - loading, - { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, - ] = useAuthentications({ docValueFields, endDate, filterQuery, startDate, type }); - return ( <> - @@ -114,4 +113,8 @@ export const AuthenticationsQueryTabBody = ({ ); }; +AuthenticationsQueryTabBodyComponent.displayName = 'AuthenticationsQueryTabBodyComponent'; + +export const AuthenticationsQueryTabBody = React.memo(AuthenticationsQueryTabBodyComponent); + AuthenticationsQueryTabBody.displayName = 'AuthenticationsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index f28c3dfa1ad7..be8412caf773 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -10,19 +10,18 @@ import { useDispatch } from 'react-redux'; import { TimelineId } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { HostsComponentsQueryProps } from './types'; -import { hostsModel } from '../../store'; import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model'; import { MatrixHistogramOption, MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from '../translations'; -import { HistogramType } from '../../../graphql/types'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; -const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; +const EVENTS_HISTOGRAM_ID = 'eventsHistogramQuery'; export const eventsStackByOptions: MatrixHistogramOption[] = [ { @@ -45,7 +44,7 @@ export const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: eventsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_EVENTS_DATA, - histogramType: HistogramType.events, + histogramType: MatrixHistogramType.events, stackByOptions: eventsStackByOptions, subtitle: undefined, title: i18n.NAVIGATION_EVENTS_TITLE, @@ -59,8 +58,8 @@ const EventsQueryTabBodyComponent: React.FC = ({ setQuery, startDate, }) => { - const { initializeTimeline } = useManageTimeline(); const dispatch = useDispatch(); + const { initializeTimeline } = useManageTimeline(); const { globalFullScreen } = useFullScreen(); useEffect(() => { initializeTimeline({ @@ -80,13 +79,11 @@ const EventsQueryTabBodyComponent: React.FC = ({ return ( <> {!globalFullScreen && ( - 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 2886089a1eb9..051e85ab310c 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 @@ -12,15 +12,14 @@ import { NetworkDnsQuery, HISTOGRAM_ID } from '../../containers/network_dns'; import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkComponentQueryProps } from './types'; -import { networkModel } from '../../store'; import { MatrixHistogramOption, MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import * as i18n from '../translations'; -import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; -import { HistogramType } from '../../../graphql/types'; +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; const NetworkDnsTableManage = manageQuery(NetworkDnsTable); @@ -37,7 +36,7 @@ export const histogramConfigs: Omit = { defaultStackByOption: dnsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? dnsStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_DNS_DATA, - histogramType: HistogramType.dns, + histogramType: MatrixHistogramType.dns, stackByOptions: dnsStackByOptions, subtitle: undefined, }; @@ -74,15 +73,13 @@ export const DnsQueryTabBody = ({ return ( <> - { - return { - useQuery: jest.fn(), - }; -}); +jest.mock('../../../common/containers/matrix_histogram', () => ({ + useMatrixHistogram: jest.fn(), +})); const theme = () => ({ eui: { ...euiDarkVars, euiSizeL: '24px' }, darkMode: true }); const from = '2020-03-31T06:00:00.000Z'; @@ -34,12 +32,14 @@ describe('Alerts by category', () => { describe('before loading data', () => { beforeAll(async () => { - (useQuery as jest.Mock).mockReturnValue({ - data: null, - loading: false, - inspect: false, - totalCount: null, - }); + (useMatrixHistogram as jest.Mock).mockReturnValue([ + false, + { + data: null, + inspect: false, + totalCount: null, + }, + ]); wrapper = mount( @@ -100,19 +100,21 @@ describe('Alerts by category', () => { describe('after loading data', () => { beforeAll(async () => { - (useQuery as jest.Mock).mockReturnValue({ - data: [ - { x: 1, y: 2, g: 'g1' }, - { x: 2, y: 4, g: 'g1' }, - { x: 3, y: 6, g: 'g1' }, - { x: 1, y: 1, g: 'g2' }, - { x: 2, y: 3, g: 'g2' }, - { x: 3, y: 5, g: 'g2' }, - ], - loading: false, - inspect: false, - totalCount: 6, - }); + (useMatrixHistogram as jest.Mock).mockReturnValue([ + false, + { + data: [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + { x: 1, y: 1, g: 'g2' }, + { x: 2, y: 3, g: 'g2' }, + { x: 3, y: 5, g: 'g2' }, + ], + inspect: false, + totalCount: 6, + }, + ]); wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx index 111935782949..1a2238c763bd 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx @@ -10,7 +10,7 @@ import { Position } from '@elastic/charts'; import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../common/constants'; import { SHOWING, UNIT } from '../../../common/components/alerts_viewer/translations'; -import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../../common/lib/keury'; import { @@ -19,7 +19,7 @@ import { IIndexPattern, Query, } from '../../../../../../../src/plugins/data/public'; -import { HostsTableType, HostsType } from '../../../hosts/store/model'; +import { HostsTableType } from '../../../hosts/store/model'; import * as i18n from '../../pages/translations'; import { @@ -107,7 +107,7 @@ const AlertsByCategoryComponent: React.FC = ({ ); return ( - = ({ headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton} id={ID} setQuery={setQuery} - sourceId="default" startDate={from} - type={HostsType.page} {...alertsByCategoryHistogramConfigs} /> ); diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index 2e9c25f01b3c..7025afde963f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -12,7 +12,7 @@ import uuid from 'uuid'; import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../common/constants'; import { SHOWING, UNIT } from '../../../common/components/events_viewer/translations'; import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; -import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; import { MatrixHistogramConfigs, MatrixHistogramOption, @@ -27,7 +27,7 @@ import { IIndexPattern, Query, } from '../../../../../../../src/plugins/data/public'; -import { HostsTableType, HostsType } from '../../../hosts/store/model'; +import { HostsTableType } from '../../../hosts/store/model'; import { InputsModelId } from '../../../common/store/inputs/constants'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; @@ -159,7 +159,7 @@ const EventsByDatasetComponent: React.FC = ({ }, [onlyField, headerChildren, eventsCountViewEventsButton]); return ( - = ({ setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} setQuery={setQuery} showSpacer={showSpacer} - sourceId="default" startDate={from} timelineId={timelineId} - type={HostsType.page} {...eventsByDatasetHistogramConfigs} title={onlyField != null ? i18n.TOP(onlyField) : eventsByDatasetHistogramConfigs.title} /> diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts index a50c9e400485..338e733b2391 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts @@ -7,6 +7,7 @@ import { FactoryQueryTypes } from '../../../../common/search_strategy/security_solution'; import { hostsFactory } from './hosts'; +import { matrixHistogramFactory } from './matrix_histogram'; import { networkFactory } from './network'; import { SecuritySolutionFactory } from './types'; @@ -15,5 +16,6 @@ export const securitySolutionFactory: Record< SecuritySolutionFactory > = { ...hostsFactory, + ...matrixHistogramFactory, ...networkFactory, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/index.ts new file mode 100644 index 000000000000..6f27f298bd69 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { buildAlertsHistogramQuery } from './query.alerts_histogram.dsl'; + +export const alertsMatrixHistogramConfig = { + buildDsl: buildAlertsHistogramQuery, + aggName: 'aggregations.alertsGroup.buckets', + parseKey: 'alerts.buckets', +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/query.alerts_histogram.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/query.alerts_histogram.dsl.ts new file mode 100644 index 000000000000..6ec6a110ec3d --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/query.alerts_histogram.dsl.ts @@ -0,0 +1,100 @@ +/* + * 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 moment from 'moment'; + +import { + createQueryFilterClauses, + calculateTimeSeriesInterval, +} from '../../../../../utils/build_query'; +import { MatrixHistogramRequestOptions } from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const buildAlertsHistogramQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, + stackByField, +}: MatrixHistogramRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'event.kind': 'alert', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeSeriesInterval(from, to); + const histogramTimestampField = '@timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: interval, + min_doc_count: 0, + extended_bounds: { + min: moment(from).valueOf(), + max: moment(to).valueOf(), + }, + }, + }; + return { + alertsGroup: { + terms: { + field: stackByField, + missing: 'All others', + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + alerts: dateHistogram, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/index.ts new file mode 100644 index 000000000000..ab273d962ae9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { buildAnomaliesHistogramQuery } from './query.anomalies_histogram.dsl'; + +export const anomaliesMatrixHistogramConfig = { + buildDsl: buildAnomaliesHistogramQuery, + aggName: 'aggregations.anomalyActionGroup.buckets', + parseKey: 'anomalies.buckets', +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/query.anomalies_histogram.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/query.anomalies_histogram.dsl.ts new file mode 100644 index 000000000000..e7e0c4b9ab56 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/query.anomalies_histogram.dsl.ts @@ -0,0 +1,81 @@ +/* + * 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 moment from 'moment'; + +import { + createQueryFilterClauses, + calculateTimeSeriesInterval, +} from '../../../../../utils/build_query'; +import { MatrixHistogramRequestOptions } from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const buildAnomaliesHistogramQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, + stackByField = 'job_id', +}: MatrixHistogramRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + timestamp: { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeSeriesInterval(from, to); + const histogramTimestampField = 'timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: interval, + min_doc_count: 0, + extended_bounds: { + min: moment(from).valueOf(), + max: moment(to).valueOf(), + }, + }, + }; + return { + anomalyActionGroup: { + terms: { + field: stackByField, + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + anomalies: dateHistogram, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggs: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/index.ts new file mode 100644 index 000000000000..17fb67e5fe94 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { buildAuthenticationsHistogramQuery } from './query.authentications_histogram.dsl'; + +export const authenticationsMatrixHistogramConfig = { + buildDsl: buildAuthenticationsHistogramQuery, + aggName: 'aggregations.eventActionGroup.buckets', + parseKey: 'events.buckets', +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/query.authentications_histogram.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/query.authentications_histogram.dsl.ts new file mode 100644 index 000000000000..a580ae7e0355 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/query.authentications_histogram.dsl.ts @@ -0,0 +1,92 @@ +/* + * 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 moment from 'moment'; + +import { + createQueryFilterClauses, + calculateTimeSeriesInterval, +} from '../../../../../utils/build_query'; +import { MatrixHistogramRequestOptions } from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const buildAuthenticationsHistogramQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, + stackByField = 'event.outcome', +}: MatrixHistogramRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + bool: { + must: [ + { + term: { + 'event.category': 'authentication', + }, + }, + ], + }, + }, + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeSeriesInterval(from, to); + const histogramTimestampField = '@timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: interval, + min_doc_count: 0, + extended_bounds: { + min: moment(from).valueOf(), + max: moment(to).valueOf(), + }, + }, + }; + return { + eventActionGroup: { + terms: { + field: stackByField, + include: ['success', 'failure'], + order: { + _count: 'desc', + }, + size: 2, + }, + aggs: { + events: dateHistogram, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/helpers.ts new file mode 100644 index 000000000000..d0fff848b426 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/helpers.ts @@ -0,0 +1,32 @@ +/* + * 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 { + MatrixHistogramData, + MatrixHistogramParseData, + DnsHistogramSubBucket, +} from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const getDnsParsedData = ( + data: MatrixHistogramParseData, + keyBucket: string +): MatrixHistogramData[] => { + let result: MatrixHistogramData[] = []; + data.forEach((bucketData: unknown) => { + const time = get('key', bucketData); + const histData = getOr([], keyBucket, bucketData).map( + // eslint-disable-next-line @typescript-eslint/naming-convention + ({ key, doc_count }: DnsHistogramSubBucket) => ({ + x: time, + y: doc_count, + g: key, + }) + ); + result = [...result, ...histData]; + }); + return result; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/index.ts new file mode 100644 index 000000000000..557e2ebf759e --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { buildDnsHistogramQuery } from './query.dns_histogram.dsl'; +import { getDnsParsedData } from './helpers'; + +export const dnsMatrixHistogramConfig = { + buildDsl: buildDnsHistogramQuery, + aggName: 'aggregations.NetworkDns.buckets', + parseKey: 'dns.buckets', + parser: getDnsParsedData, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/query.dns_histogram.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/query.dns_histogram.dsl.ts new file mode 100644 index 000000000000..08a080865dfc --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/query.dns_histogram.dsl.ts @@ -0,0 +1,84 @@ +/* + * 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, + calculateTimeSeriesInterval, +} from '../../../../../utils/build_query'; +import { MatrixHistogramRequestOptions } from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const buildDnsHistogramQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, + stackByField, +}: MatrixHistogramRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeSeriesInterval(from, to); + const histogramTimestampField = '@timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: interval, + }, + }; + + return { + NetworkDns: { + ...dateHistogram, + aggs: { + dns: { + terms: { + field: stackByField, + order: { + orderAgg: 'desc', + }, + size: 10, + }, + aggs: { + orderAgg: { + cardinality: { + field: 'dns.question.name', + }, + }, + }, + }, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/index.ts new file mode 100644 index 000000000000..051436ee6c69 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { buildEventsHistogramQuery } from './query.events_histogram.dsl'; + +export const eventsMatrixHistogramConfig = { + buildDsl: buildEventsHistogramQuery, + aggName: 'aggregations.eventActionGroup.buckets', + parseKey: 'events.buckets', +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.ts new file mode 100644 index 000000000000..d3b85872c5f0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.ts @@ -0,0 +1,92 @@ +/* + * 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 moment from 'moment'; + +import { showAllOthersBucket } from '../../../../../../common/constants'; +import { + createQueryFilterClauses, + calculateTimeSeriesInterval, +} from '../../../../../utils/build_query'; +import { MatrixHistogramRequestOptions } from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; +import * as i18n from './translations'; + +export const buildEventsHistogramQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, + stackByField = 'event.action', +}: MatrixHistogramRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeSeriesInterval(from, to); + const histogramTimestampField = '@timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: interval, + min_doc_count: 0, + extended_bounds: { + min: moment(from).valueOf(), + max: moment(to).valueOf(), + }, + }, + }; + + const missing = + stackByField != null && showAllOthersBucket.includes(stackByField) + ? { + missing: stackByField?.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, + } + : {}; + + return { + eventActionGroup: { + terms: { + field: stackByField, + ...missing, + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + events: dateHistogram, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/translations.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/translations.ts new file mode 100644 index 000000000000..0e46f5cff144 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/translations.ts @@ -0,0 +1,14 @@ +/* + * 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 ALL_OTHERS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel', + { + defaultMessage: 'All others', + } +); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.ts new file mode 100644 index 000000000000..f306518fc335 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.ts @@ -0,0 +1,33 @@ +/* + * 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 { + MatrixHistogramParseData, + MatrixHistogramBucket, + MatrixHistogramData, +} from '../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const getGenericData = ( + data: MatrixHistogramParseData, + keyBucket: string +): MatrixHistogramData[] => { + let result: MatrixHistogramData[] = []; + data.forEach((bucketData: unknown) => { + const group = get('key', bucketData); + const histData = getOr([], keyBucket, bucketData).map( + // eslint-disable-next-line @typescript-eslint/naming-convention + ({ key, doc_count }: MatrixHistogramBucket) => ({ + x: key, + y: doc_count, + g: group, + }) + ); + result = [...result, ...histData]; + }); + + return result; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts new file mode 100644 index 000000000000..9cee2c0f1dc4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts @@ -0,0 +1,75 @@ +/* + * 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 { + FactoryQueryTypes, + MatrixHistogramRequestOptions, + MatrixHistogramStrategyResponse, + MatrixHistogramQuery, + MatrixHistogramType, + MatrixHistogramDataConfig, +} from '../../../../../common/search_strategy/security_solution'; +import { inspectStringifyObject } from '../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../types'; +import { getGenericData } from './helpers'; +import { alertsMatrixHistogramConfig } from './alerts'; +import { anomaliesMatrixHistogramConfig } from './anomalies'; +import { authenticationsMatrixHistogramConfig } from './authentications'; +import { dnsMatrixHistogramConfig } from './dns'; +import { eventsMatrixHistogramConfig } from './events'; + +const matrixHistogramConfig: MatrixHistogramDataConfig = { + [MatrixHistogramType.alerts]: alertsMatrixHistogramConfig, + [MatrixHistogramType.anomalies]: anomaliesMatrixHistogramConfig, + [MatrixHistogramType.authentications]: authenticationsMatrixHistogramConfig, + [MatrixHistogramType.dns]: dnsMatrixHistogramConfig, + [MatrixHistogramType.events]: eventsMatrixHistogramConfig, +}; + +export const matrixHistogram: SecuritySolutionFactory = { + buildDsl: (options: MatrixHistogramRequestOptions) => { + const myConfig = getOr(null, options.histogramType, matrixHistogramConfig); + if (myConfig == null) { + throw new Error(`This histogram type ${options.histogramType} is unknown to the server side`); + } + return myConfig.buildDsl(options); + }, + parse: async ( + options: MatrixHistogramRequestOptions, + response: IEsSearchResponse + ): Promise => { + const myConfig = getOr(null, options.histogramType, matrixHistogramConfig); + if (myConfig == null) { + throw new Error(`This histogram type ${options.histogramType} is unknown to the server side`); + } + const totalCount = getOr(0, 'hits.total.value', response.rawResponse); + const matrixHistogramData = getOr([], myConfig.aggName, response.rawResponse); + const inspect = { + dsl: [inspectStringifyObject(myConfig.buildDsl(options))], + }; + const dataParser = myConfig.parser ?? getGenericData; + + return { + ...response, + inspect, + matrixHistogramData: dataParser( + matrixHistogramData, + myConfig.parseKey + ), + totalCount, + }; + }, +}; + +export const matrixHistogramFactory: Record< + typeof MatrixHistogramQuery, + SecuritySolutionFactory +> = { + [MatrixHistogramQuery]: matrixHistogram, +};